package com.pokermind.operator.service;

import com.pokermind.common.context.RequestContext;
import com.pokermind.common.log.PlainLog;
import com.pokermind.common.log.TimeCostLog;
import com.pokermind.common.util.JsonUtil;
import com.pokermind.model.dto.PokerAIRequest;
import com.pokermind.model.dto.PokerAIResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.*;

/**
 * 德州扑克AI服务 - 基于Spring AI框架 直接调用AI服务，错误时抛异常，无降级逻辑
 *
 * @author PokerMind Team
 */
@Service
public class PokerAIService {

    private static final Logger logger = LoggerFactory.getLogger(PokerAIService.class);

    @Autowired(required = false)
    private ChatClient defaultChatClient;
    
    @Autowired(required = false)
    @Qualifier("qwenChatClient")
    private ChatClient qwenChatClient;

    @Autowired(required = false)
    @Qualifier("siliconflowChatClient")
    private ChatClient siliconflowChatClient;
    
    @Value("${pokermind.ai.provider:qwen}")
    private String provider;

    /**
     * 德州扑克AI决策分析
     *
     * @param request 扑克分析请求
     * @return AI决策结果
     * @throws RuntimeException 当AI服务不可用或调用失败时
     */
    public PokerAIResponse analyzePokerDecision(PokerAIRequest request) {
        return analyzePokerDecision(request, null);
    }

    /**
     * 德州扑克AI决策分析 - 支持指定AI提供商
     *
     * @param request           扑克分析请求
     * @param preferredProvider 首选AI提供商 (qwen/siliconflow)，null则使用配置的provider
     * @return AI决策结果
     * @throws RuntimeException 当AI服务不可用或调用失败时
     */
    public PokerAIResponse analyzePokerDecision(PokerAIRequest request, String preferredProvider) {
        PlainLog log = initializeLog(request);
        
        try {
            ChatClient client = selectChatClient(preferredProvider);
            if (client == null) {
                throw new RuntimeException("No AI provider available");
            }
            
            String providerName = getProviderName(client);
            String prompt = buildPokerPrompt(request);
            log.put("provider", providerName).put("promptLen", prompt.length());
            logPrompt(prompt);

            String response = invokeAI(client, prompt, log);
            logResponse(response);
            
            PokerAIResponse aiResponse = parseAIResponse(response);
            log.put("aiAction", aiResponse.getAction()).put("aiSize", aiResponse.getSize()).put("op_rslt", "success");
            logger.info(log.toString());
            return aiResponse;
        } catch (Exception e) {
            logger.error(log.put("op_rslt", "failed").put("error", e.getMessage()).toString(), e);
            throw e;
        }
    }

    private PlainLog initializeLog(PokerAIRequest request) {
        return new TimeCostLog("op", "poker_ai_call").put("reqId", RequestContext.getRequestId())
            .put("playerId", request.getPlayerId())
            .put("playerPos", request.getPlayerPos() != null ? request.getPlayerPos() : request.getHeroPos())
            .put("street", request.getStreet())
            .put("playerEquity", request.getPlayerEquity() != null ? 
                String.format("%.3f", request.getPlayerEquity()) : 
                (request.getHeroEquity() != null ? String.format("%.3f", request.getHeroEquity()) : null));
    }

    private String invokeAI(ChatClient client, String prompt, PlainLog log) {
        var promptRequest = client.prompt();
        if (promptRequest == null) {
            throw new RuntimeException("ChatClient prompt request is null");
        }

        long startTime = System.currentTimeMillis();
        var chatResponse = promptRequest.user(prompt).call().chatResponse();
        long duration = System.currentTimeMillis() - startTime;

        String response = extractContentFromChatResponse(chatResponse);

        log.put("aiCallTime", duration).put("responseLen", response.length());
        enrichLogWithMetadata(log, chatResponse, getProviderName(client));

        return response;
    }

    private void enrichLogWithMetadata(PlainLog log, org.springframework.ai.chat.model.ChatResponse chatResponse,
        String provider) {
        if (chatResponse == null || chatResponse.getMetadata() == null) {
            return;
        }

        log.put("model", determineActualModel(chatResponse, provider));

        var usage = chatResponse.getMetadata().getUsage();
        if (usage != null) {
            log.put("promptTokens", usage.getPromptTokens())
                .put("completionTokens", usage.getTotalTokens() - usage.getPromptTokens())
                .put("totalTokens", usage.getTotalTokens());
        }
    }

    private void logPrompt(String prompt) {
        String reqId = RequestContext.getRequestId();
        logger.info("{}=== AI Input (Prompt) ===\n{}\n=========================",
            reqId != null ? "[ReqID:" + reqId + "] " : "", prompt);
    }

    private void logResponse(String response) {
        String reqId = RequestContext.getRequestId();
        logger.info("{}=== AI Output (Response) ===\n{}\n============================",
            reqId != null ? "[ReqID:" + reqId + "] " : "", response);
    }

    /**
     * 从ChatResponse中提取纯文本内容
     */
    private String extractContentFromChatResponse(org.springframework.ai.chat.model.ChatResponse chatResponse) {
        if (chatResponse == null || chatResponse.getResult() == null) {
            return "";
        }

        var assistantMessage = chatResponse.getResult().getOutput();
        if (assistantMessage == null) {
            return "";
        }

        // 尝试多种方式提取内容
        // 方式1：检查metadata中的textContent
        if (assistantMessage.getMetadata() != null && assistantMessage.getMetadata().containsKey("textContent")) {
            return assistantMessage.getMetadata().get("textContent").toString();
        }

        // 方式2：从toString中解析textContent
        String fullString = assistantMessage.toString();
        int startIdx = fullString.indexOf("textContent=");
        if (startIdx != -1) {
            startIdx += "textContent=".length();
            int endIdx = fullString.indexOf(", metadata=", startIdx);
            if (endIdx == -1) {
                endIdx = fullString.indexOf("]", startIdx);
            }
            if (endIdx > startIdx) {
                return fullString.substring(startIdx, endIdx);
            }
        }

        // 方式3：直接返回toString（fallback）
        return fullString;
    }

    /**
     * 选择合适的ChatClient
     * @param preferredProvider 指定provider (qwen/siliconflow)，null则使用defaultChatClient
     */
    private ChatClient selectChatClient(String preferredProvider) {
        // 如果指定了provider，优先使用指定的
        if (preferredProvider != null) {
            if ("siliconflow".equalsIgnoreCase(preferredProvider) && siliconflowChatClient != null) {
                return siliconflowChatClient;
            }
            if ("qwen".equalsIgnoreCase(preferredProvider) && qwenChatClient != null) {
                return qwenChatClient;
            }
        }

        // 使用defaultChatClient（已根据配置选择）
        return defaultChatClient;
    }

    /**
     * 构建德州扑克AI决策Prompt（V4.10 - GTO增强版）
     * 
     * V4.10改进（GTO决策增强）：
     * 1. 新增GTO分析部分：显示GTO Solver计算的频率分布
     * 2. 移除简单EV显示：避免误导（EV计算过于简化）
     * 3. 强化决策指引：明确GTO建议的权重
     * 
     * V4.9改进：
     * 1. 当前局面：增加需跟注、可加注范围等更多信息
     * 2. 数学依据：增强EV、概率、隐含赔率等核心指标计算
     * 3. 移除对手信息板块（暂时不需要）
     * 4. reasons改为专业描述
     * 5. size字段只返回数字（无单位）
     * 
     * @param request AI请求对象
     * @param gtoFrequencies GTO频率分布（可选）
     */
    public String buildPokerPrompt(PokerAIRequest request, Map<String, Double> gtoFrequencies) {
        StringBuilder p = new StringBuilder();

        // AI角色定位（V4.17 LLM优化）
        p.append("你是Daniel级别职业德州扑克玩家，15年实战，WSOP多次冠军，精通PioSolver/GTO+等Solver工具。核心：数学驱动、位置优先、EV最大化。\n\n");
        
        // V4.31：核心约束前置（最高优先级）
        p.append("【⚠️ 核心约束 - 必须严格遵守】\n");
        p.append("1. 数值比较必须逻辑正确，绝对禁止表述相反\n");
        p.append("   ✅ 正确: \"胜率28.4高于阈值25.0\"\n");
        p.append("   ❌ 错误: \"胜率28.4低于阈值25.0\"\n\n");
        p.append("2. 若胜率高于阈值但仍fold/call，必须说明额外原因：\n");
        p.append("   模板: \"胜率X虽高于阈值Y，但[对手范围收缩/SPR不利/位置劣势]，[action]最优\"\n\n");
        p.append("3. Reasoning必须遵循三段式结构：\n");
        p.append("   [底牌评价] → [数值对比与逻辑] → [决策依据]\n\n");
        
        // V4.36：牌面表示格式说明（确保大模型正确理解）
        p.append("【📋 牌面表示格式】\n");
        p.append("• 点数：A(Ace), 2-9, T(Ten), J(Jack), Q(Queen), K(King)\n");
        p.append("• 花色：h(红桃♥), d(方块♦), c(梅花♣), s(黑桃♠)\n");
        p.append("• 示例：Ah=红桃A, Kd=方块K, Jh3s=红桃J+黑桃3\n");
        p.append("• 在reasoning中直接使用这种格式描述手牌（如\"Jh3s是较弱的起手牌...\"）\n\n");
        
        p.append("=== GTO决策框架 (LLM优化版) ===\n\n");
        p.append("EV为王 (EV First): 永远选择长期收益最高的行动。评估时，不仅看当前，还要考虑这张牌未来的发展潜力（隐含赔率）和风险（反向隐含赔率）。\n\n");
        p.append("范围思考 (Range Thinking): 别只看你的牌，要思考你和对手所有可能的手牌范围。当前牌面对谁的范围更有利（范围优势）？谁的范围里有更多超强牌（坚果优势）？\n");
        p.append("【V4.30关键】多轮3-bet后对手范围极度收缩：\n");
        p.append("  • 2轮加注：对手可能有{JJ+, AK, maybe AQ}\n");
        p.append("  • 3轮加注：对手很可能只有{QQ+, AKs, maybe AKo}\n");
        p.append("  • 4轮加注：对手几乎只有{KK+, maybe AKs}\n");
        p.append("  • 5轮+加注：对手只有坚果牌{AA,KK}，继续raise通常是错误的\n");
        p.append("你的胜率需要根据对手的【实际范围】重新评估，而不是假设对手有宽范围\n\n");
        p.append("平衡下注 (Balanced Betting): 你的下注里要有价值牌（为了让更差的牌跟注）和诈唬牌（为了让更好的牌弃牌）。根据牌面结构和范围优势选择下注尺寸：\n");
        p.append("  • 优势大/牌面两极化 (A-K-2): 用大尺寸下注（强牌+诈唬）\n");
        p.append("  • 优势小/牌面连接性强 (T-9-8): 用小尺寸下注（更宽的价值牌）\n\n");
        p.append("位置就是生命 (Position is Power): 你在按钮位(BTN)，拥有最终行动权和信息优势。利用这个优势，你可以更积极地行动。对手在不利位置，他们的行动更值得警惕。\n\n");
        p.append("SPR决定策略 (SPR Dictates Strategy): 检查有效筹码和底池的比例(SPR)。\n");
        p.append("  • 低SPR (<4): 顶对就是强牌，倾向于打到全下\n");
        p.append("  • 高SPR (>10): 听牌和超强牌更有价值，小心用顶对玩大底池\n\n");
        p.append("剥削对手 (Exploit Opponent): GTO是基础，但如果对手有明显漏洞，就要调整。\n");
        p.append("  • 对手是跟注站 (总跟注): 多做价值下注，别诈唬\n");
        p.append("  • 对手是软弱玩家 (总弃牌): 多诈唬\n\n");

        // 当前局面（V4.10 Token优化：去冗余）
        p.append("=== 当前局面 ===\n");
        
        p.append("街道：").append(translateStreetSimple(request.getStreet())).append("\n");
        
        // 使用新字段，向后兼容旧字段
        String pos = request.getPlayerPos() != null ? request.getPlayerPos() : request.getHeroPos();
        p.append("位置：").append(pos);
        
        // V4.10：位置只显示名称，不添加策略性描述
        p.append("\n");
        
        // 底牌：优先使用新字段holeCards
        if (request.getHoleCards() != null && !request.getHoleCards().isEmpty()) {
            p.append("底牌：").append(String.join(" ", request.getHoleCards())).append("\n");
        } else if (request.getHeroHole() != null) {
            p.append("底牌：").append(request.getHeroHole()).append("\n");
        }
        
        // 公共牌（如果有）
        if (request.getBoard() != null && !request.getBoard().isEmpty()) {
            p.append("公共牌：").append(request.getBoard()).append("\n");
        }
        
        // 底池和下注信息
        Double pot = request.getPot();
        Double toCall = request.getAmountToCall();
        Double playerStack = request.getStacks();
        
        if (pot != null) {
            p.append("底池：").append(String.format("%.0f", pot)).append("\n");
        }
        
        // 当前下注额
        if (toCall != null && toCall > 0) {
            p.append("需跟注：").append(String.format("%.0f", toCall)).append("\n");
        }
        
        // 你的筹码（V4.16：去除所有单位）
        if (playerStack != null) {
            p.append("你的筹码：").append(String.format("%.0f", playerStack));
            if (request.getBigBlind() != null) {
                double bb = playerStack / request.getBigBlind();
                p.append(" (").append(String.format("%.1f", bb)).append("大盲)");
            }
            p.append("\n");
        }
        
        // 对手信息（V4.16重构：显示对手筹码）
        String street = request.getStreet();
        int opponentCount = calculateOpponentCount(request);
        
        // 显示对手筹码列表
        appendOpponentStacks(p, request);
        
        if ("preflop".equalsIgnoreCase(street)) {
            // Preflop：显示对手人数（更清晰的描述）
            if (opponentCount > 0) {
                if (opponentCount == 1) {
                    p.append("对手：1人（单挑）\n\n");
                } else {
                    p.append("对手：").append(opponentCount).append("人（多人底池）\n\n");
                }
            } else {
                p.append("\n");
            }
        } else {
            // Postflop：显示在局人数
            if (opponentCount == 1) {
                p.append("对手：1人（单挑）\n\n");
            } else {
                p.append("对手：").append(opponentCount).append("人（多人底池）\n\n");
            }
        }

        // 数学依据（V4.16全面重构：去除所有单位）
        p.append("=== 数学依据 ===\n");
        
        // 优先使用新字段，向后兼容旧字段
        Double playerEquity = request.getPlayerEquity() != null ? request.getPlayerEquity() : request.getHeroEquity();
        
        // 1. 胜率（去除%）
        if (playerEquity != null) {
            p.append("胜率：").append(String.format("%.1f", playerEquity * 100)).append("\n");
        }
        
        // 2. SPR（Stack-to-Pot Ratio）- 自动计算
        Double spr = request.getSpr();
        if (spr == null && playerStack != null && pot != null && pot > 0) {
            spr = playerStack / pot;
        }
        if (spr != null) {
            p.append("SPR：").append(String.format("%.1f", spr)).append("\n");
        }
        
        // 3. 底池赔率（V4.10：纯数值，不添加说明）
        if (toCall != null && toCall > 0 && pot != null) {
            double potOdds = toCall / (pot + toCall);
            p.append("底池赔率：").append(String.format("%.1f", potOdds * 100)).append("\n");
        }
        
        // 4. 需跟注（去除%，已在"当前局面"显示）
        if (toCall != null && toCall == 0) {
            p.append("需跟注：0\n");
        }
        
        // 5. 有效筹码（已在"当前局面"显示，这里跳过）
        
        // 6. EV对比（V4.10：已移除，简单EV计算容易误导AI）
        // 原因：简单EV计算忽略了位置劣势、牌力可实现性、反向隐含赔率等关键因素
        // 改为使用GTO频率分布指导决策
        
        // 7. 手牌类型（保留）
        String handCategory = classifyHand(request);
        if (handCategory != null) {
            p.append("手牌类型：").append(handCategory).append("\n");
        }
        
        // 8. 威胁评估（Preflop时，V4.21优化：区分位置风险）
        if ("preflop".equalsIgnoreCase(street) && (toCall == null || toCall == 0)) {
            String position = request.getPlayerPos() != null ? request.getPlayerPos() : request.getHeroPos();
            int remaining = calculateOpponentCount(request);
            
            // 只有后续人数>=3时才显示3-bet风险
            if (remaining >= 3) {
                // 根据位置调整3-bet风险系数
                double riskFactor = 0.015; // 默认每人1.5%
                if ("UTG".equals(position) || "EP".equals(position)) {
                    riskFactor = 0.018; // 早期位置风险略高，每人1.8%
                } else if ("MP".equals(position) || "HJ".equals(position)) {
                    riskFactor = 0.015; // 中期位置，每人1.5%
                } else if ("CO".equals(position)) {
                    riskFactor = 0.012; // 后期位置风险略低，每人1.2%
                }
                
                double threeBetProb = Math.min(remaining * riskFactor, 0.12); // 上限12%
                p.append("后续被反加注概率：").append(String.format("%.1f", threeBetProb * 100)).append("\n");
                
                // V4.10：只显示防御阈值数值，不添加判断
                if (("UTG".equals(position) || "EP".equals(position) || "MP".equals(position)) && playerEquity != null) {
                    double defenseThreshold = 0.45;
                    p.append("防御反加注需胜率：").append(String.format("%.1f", defenseThreshold * 100)).append("\n");
                }
            }
        }
        
        // 9. GTO阈值（V4.31：明确应用场景）
        if ("preflop".equalsIgnoreCase(street)) {
            String position = request.getPlayerPos() != null ? request.getPlayerPos() : request.getHeroPos();
            if (playerEquity != null) {
                double gtoThreshold = getGTOEquityThreshold(position);
                // V4.31: 明确说明这是RFI场景的参考，facing raise时需考虑对手范围
                p.append("GTO阈值：").append(String.format("%.1f", gtoThreshold * 100));
                p.append("（").append(position).append("位RFI参考");
                
                // 如果是facing raise，添加重要提示
                if (toCall != null && toCall > 0) {
                    p.append("，当前facing raise需考虑对手范围收缩");
                }
                p.append("）\n");
            }
        }
        
        // V4.10: 10. GTO频率（纯数值参考，不做过多设定）
        if (gtoFrequencies != null && !gtoFrequencies.isEmpty()) {
            p.append("\n=== GTO ===\n");
            
            // 按频率降序排列
            java.util.List<java.util.Map.Entry<String, Double>> sortedActions = gtoFrequencies.entrySet()
                .stream()
                .sorted(java.util.Map.Entry.<String, Double>comparingByValue().reversed())
                .collect(java.util.stream.Collectors.toList());
            
            // 只显示纯数值，让AI自己判断
            for (java.util.Map.Entry<String, Double> entry : sortedActions) {
                String action = entry.getKey();
                double freq = entry.getValue() * 100;
                p.append(action).append(": ").append(String.format("%.0f%%", freq)).append("\n");
            }
        }
        
        p.append("\n");
        
        // 行动历史（简洁）
        appendActionsKey(p, request.getActionsHistory());
        
        // V4.30: 行动历史分析（多轮加注警告）
        appendActionHistoryAnalysis(p, request);

        // 第5层：决策要求（无硬性规则）
        appendDecisionRequest(p, request);

        return p.toString();
    }
    
    /**
     * 构建Prompt - 兼容性方法（无GTO频率）
     * 
     * @param request AI请求对象
     * @return Prompt字符串
     */
    public String buildPokerPrompt(PokerAIRequest request) {
        return buildPokerPrompt(request, null);
    }
    
    /**
     * 所有玩家详细信息
     */
    private void appendAllPlayersDetailed(StringBuilder p, PokerAIRequest request) {
        if (request.getActionsBrief() == null) {
            return;
        }

        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> players = JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null || players.isEmpty()) {
                return;
            }

            p.append("=== 所有玩家状态 ===\n");
            for (Map<String, Object> player : players) {
                String pos = (String)player.get("pos");
                Number stackNum = (Number)player.get("stack");
                String status = (String)player.get("status");
                Object isHeroObj = player.get("isHero");
                boolean isHero = isHeroObj != null && (Boolean)isHeroObj;

                p.append(translatePosition(pos)).append(": ");
                
                if (stackNum != null) {
                    p.append(String.format("%.2f", stackNum.doubleValue()));
                }
                
                if ("已弃牌".equals(status)) {
                    p.append(" [已弃牌]");
                } else if ("在局".equals(status)) {
                    p.append(" [在局]");
                }
                
                if (isHero) {
                    p.append(" ← Hero");
                }
                
                p.append("\n");
            }
            p.append("\n");
        } catch (Exception e) {
            logger.debug("Failed to parse players info", e);
        }
    }

    /**
     * 完整行动历史（按街道详细展示）
     */
    private void appendActionsFullHistory(StringBuilder p, List<PokerAIRequest.ActionHistory> actions) {
        if (actions == null || actions.isEmpty()) {
            return;
        }

        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet();
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }

        p.append("=== 行动历史 ===\n");
        String[] streetOrder = {"preflop", "flop", "turn", "river"};

        for (String street : streetOrder) {
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty()) {
                continue;
            }

            p.append(translateStreet(street)).append(":\n");

            for (PokerAIRequest.ActionHistory action : streetActions) {
                String pos = action.getPosition();
                String act = action.getAction();
                String size = action.getSize();

                p.append("  - ").append(translatePosition(pos)).append(": ");
                p.append(translateAction(act, size));
                p.append("\n");
            }
        }
        p.append("\n");
    }

    /**
     * 完整数学分析
     */
    private void appendMathAnalysisFull(StringBuilder p, PokerAIRequest request) {
        p.append("=== 数学分析 ===\n");

        // Hero胜率
        if (request.getHeroEquity() != null) {
            p.append("Hero胜率: ").append(String.format("%.1f", request.getHeroEquity() * 100));
            double equity = request.getHeroEquity();
            if (equity > 0.70) {
                p.append(" (强牌力)");
            } else if (equity > 0.50) {
                p.append(" (中等牌力)");
            } else if (equity > 0.30) {
                p.append(" (弱牌力)");
            } else {
                p.append(" (极弱牌力)");
            }
            p.append("\n");
        }

        // 根据场景显示不同的数学分析
        if (request.getCanCheck() != null && request.getCanCheck()) {
            // 场景1: 可以免费过牌
            p.append("当前无需跟注: Hero可以免费看牌(check)或主动下注(bet)\n");
            if (request.getHeroEquity() != null) {
                double equity = request.getHeroEquity();
                if (equity > 0.65) {
                    p.append("下注建议: 牌力强，建议主动下注建立价值\n");
                } else if (equity > 0.45) {
                    p.append("下注建议: 牌力中等，可以根据位置和对手倾向选择过牌或下注\n");
                } else {
                    p.append("下注建议: 牌力较弱，建议过牌控池或弃牌\n");
                }
            }
        } else if (request.getFacingBet() != null && request.getFacingBet()) {
            // 场景2: 面临下注，底池赔率分析
            if (request.getPotOddsNote() != null) {
                String note = request.getPotOddsNote();
                if (note.contains("需跟注$") && note.contains("底池$")) {
                try {
                    int toCallStart = note.indexOf("需跟注$") + 4;
                    int toCallEnd = note.indexOf("，", toCallStart);
                    int potStart = note.indexOf("底池$") + 3;
                    int potEnd = note.indexOf("（", potStart);

                    if (toCallEnd > toCallStart && potEnd > potStart) {
                        double toCall = Double.parseDouble(note.substring(toCallStart, toCallEnd));
                        double pot = Double.parseDouble(note.substring(potStart, potEnd));
                        double equity = request.getHeroEquity() != null ? request.getHeroEquity() : 0.5;

                        double potOdds = toCall / (pot + toCall);
                        double requiredEquity = potOdds;
                        double ev = (equity * (pot + toCall)) - ((1 - equity) * toCall);

                        p.append("需跟注: ").append(String.format("%.2f", toCall)).append("\n");
                        p.append("跟注后底池: ").append(String.format("%.2f", pot + toCall)).append("\n");
                        p.append("底池赔率: ").append(String.format("%.1f", potOdds * 100));
                        p.append(" (需投入").append(String.format("%.2f", toCall))
                            .append("赢取").append(String.format("%.2f", pot + toCall)).append(")\n");
                        p.append("所需胜率: ").append(String.format("%.1f", requiredEquity * 100))
                            .append(" (盈亏平衡点)\n");
                        
                        p.append("期望值(EV): ");
                        if (ev > 0) {
                            p.append("+").append(String.format("%.2f", ev))
                                .append(" (正EV，数学上有利可图)");
                        } else if (ev < 0) {
                            p.append("-").append(String.format("%.2f", Math.abs(ev)))
                                .append(" (负EV，数学上不利)");
                        } else {
                            p.append("0.00 (盈亏平衡)");
                        }
                        p.append("\n");

                        // EV比较
                        if (equity > requiredEquity + 0.10) {
                            p.append("结论: Hero胜率(").append(String.format("%.1f", equity * 100))
                                .append(")明显高于所需胜率(").append(String.format("%.1f", requiredEquity * 100))
                                .append(")，数学上应该跟注或加注\n");
                        } else if (equity > requiredEquity) {
                            p.append("结论: Hero胜率略高于所需胜率，可以考虑跟注\n");
                        } else {
                            p.append("结论: Hero胜率低于所需胜率，数学上应该弃牌\n");
                        }
                    }
                } catch (Exception ignored) {
                }
            }
            }
        }

        // SPR分析
        if (request.getSpr() != null) {
            double spr = request.getSpr();
            p.append("SPR(筹码底池比): ").append(String.format("%.1f", spr));
            if (spr < 3.0) {
                p.append(" (浅筹码局面，适合用强牌全压，不适合投机)\n");
            } else if (spr < 6.0) {
                p.append(" (中浅筹码，可以用强牌和强听牌全压)\n");
            } else if (spr < 13.0) {
                p.append(" (中等筹码，需要平衡价值下注和控池)\n");
            } else {
                p.append(" (深筹码局面，需要谨慎决策，避免陷入大底池)\n");
            }
        }

        p.append("\n");
    }

    /**
     * 简化的街道翻译
     */
    private String translateStreetSimple(String street) {
        if (street == null) return "";
        switch (street.toLowerCase()) {
            case "preflop": return "翻前";
            case "flop": return "翻牌";
            case "turn": return "转牌";
            case "river": return "河牌";
            default: return street;
        }
    }

    /**
     * 简化的位置翻译
     */
    private String translatePositionSimple(String pos) {
        if (pos == null) return "";
        switch (pos.toUpperCase()) {
            case "SB": return "小盲";
            case "BB": return "大盲";
            case "UTG": return "枪口";
            case "MP": return "中位";
            case "CO": return "关煞";
            case "BTN": return "按钮";
            default: return pos;
        }
    }

    /**
     * 玩家信息（紧凑格式：位置$筹码[状态]）
     */
    private void appendPlayersCompact(StringBuilder p, PokerAIRequest request) {
        if (request.getActionsBrief() == null) {
            return;
        }

        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> players = JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null || players.isEmpty()) {
                return;
            }

            p.append("【玩家】");
            boolean first = true;
            for (Map<String, Object> player : players) {
                String pos = (String)player.get("pos");
                Number stackNum = (Number)player.get("stack");
                String status = (String)player.get("status");
                Object isHeroObj = player.get("isHero");
                boolean isHero = isHeroObj != null && (Boolean)isHeroObj;

                if (!first) p.append(" ");
                first = false;

                p.append(translatePositionSimple(pos));
                if (stackNum != null) {
                    p.append("$").append(String.format("%.0f", stackNum.doubleValue()));
                }
                if ("已弃牌".equals(status)) {
                    p.append("[弃]");
                }
                if (isHero) {
                    p.append("★");
                }
            }
            p.append("\n");
        } catch (Exception e) {
            logger.debug("Failed to parse players info", e);
        }
    }

    /**
     * 行动历史（紧凑格式：街道: 位置-动作$金额）
     */
    private void appendActionsCompact(StringBuilder p, List<PokerAIRequest.ActionHistory> actions) {
        if (actions == null || actions.isEmpty()) {
            return;
        }

        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet();
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }

        p.append("【历史】");
        String[] streetOrder = {"preflop", "flop", "turn", "river"};
        boolean firstStreet = true;

        for (String street : streetOrder) {
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty()) {
                continue;
            }

            if (!firstStreet) p.append(" | ");
            firstStreet = false;

            p.append(translateStreetSimple(street)).append(":");
            for (int i = 0; i < streetActions.size(); i++) {
                if (i > 0) p.append(" ");
                PokerAIRequest.ActionHistory action = streetActions.get(i);
                p.append(translatePositionSimple(action.getPosition()));
                p.append(translateActionShort(action.getAction()));
                if (action.getSize() != null && !action.getSize().isEmpty()) {
                    p.append("$").append(action.getSize());
                }
            }
        }
        p.append("\n");
    }

    /**
     * 超简短的动作翻译
     */
    private String translateActionShort(String action) {
        if (action == null) return "";
        String lower = action.toLowerCase();
        if (lower.contains("fold")) return "弃";
        if (lower.contains("check")) return "过";
        if (lower.contains("call")) return "跟";
        if (lower.contains("raise")) return "加";
        if (lower.contains("bet")) return "下";
        if (lower.contains("small_blind")) return "s";
        if (lower.contains("big_blind")) return "b";
        return action.substring(0, 1);
    }

    /**
     * 数学分析（紧凑格式）
     */
    private void appendMathCompact(StringBuilder p, PokerAIRequest request) {
        p.append("【数学】");

        // 胜率
        if (request.getHeroEquity() != null) {
            p.append("胜率").append(String.format("%.0f%%", request.getHeroEquity() * 100));
        }

        // 底池赔率和EV
        if (request.getPotOddsNote() != null) {
            String note = request.getPotOddsNote();
            if (note.contains("需跟注$") && note.contains("底池$")) {
                try {
                    int toCallStart = note.indexOf("需跟注$") + 4;
                    int toCallEnd = note.indexOf("，", toCallStart);
                    int potStart = note.indexOf("底池$") + 3;
                    int potEnd = note.indexOf("（", potStart);

                    if (toCallEnd > toCallStart && potEnd > potStart) {
                        double toCall = Double.parseDouble(note.substring(toCallStart, toCallEnd));
                        double pot = Double.parseDouble(note.substring(potStart, potEnd));
                        double equity = request.getHeroEquity() != null ? request.getHeroEquity() : 0.5;

                        double potOdds = toCall / (pot + toCall);
                        double ev = (equity * (pot + toCall)) - ((1 - equity) * toCall);

                        p.append(" 赔率").append(String.format("%.0f%%", potOdds * 100));
                        p.append(" EV");
                        if (ev > 0) {
                            p.append("+").append(String.format("%.0f", ev));
                        } else {
                            p.append(String.format("%.0f", ev));
                        }
                    }
                } catch (Exception ignored) {
                }
            }
        }

        // SPR
        if (request.getSpr() != null) {
            double spr = request.getSpr();
            p.append(" SPR").append(String.format("%.0f", spr));
            if (spr < 3.0) {
                p.append("(浅)");
            } else if (spr > 10.0) {
                p.append("(深)");
            }
        }

        p.append("\n");
    }
    
    /**
     * 翻译街道名称
     */
    private String translateStreet(String street) {
        if (street == null) return "未知";
        switch (street.toLowerCase()) {
            case "preflop": return "翻牌前(Preflop)";
            case "flop": return "翻牌(Flop)";
            case "turn": return "转牌(Turn)";
            case "river": return "河牌(River)";
            default: return street;
        }
    }
    
    /**
     * 分类手牌类型（V4.8新增）
     */
    private String classifyHand(PokerAIRequest request) {
        // 简化版本：基于胜率和牌面分类
        String street = request.getStreet();
        List<String> holeCards = request.getHoleCards();
        String board = request.getBoard();
        Double equity = request.getPlayerEquity() != null ? request.getPlayerEquity() : request.getHeroEquity();
        
        if ("preflop".equalsIgnoreCase(street)) {
            // Preflop: 根据底牌分类
            if (holeCards != null && holeCards.size() == 2) {
                String card1 = holeCards.get(0);
                String card2 = holeCards.get(1);
                // 简化判断：同花、对子、大牌等
                if (card1.charAt(0) == card2.charAt(0)) {
                    return "对子 (Pocket Pair)";
                } else if (card1.charAt(1) == card2.charAt(1)) {
                    return "同花 (Suited)";
                } else {
                    return "非同花 (Offsuit)";
                }
            }
            return null;
        }
        
        // Postflop: 根据胜率和牌面分类
        if (equity != null) {
            if (equity >= 0.8) {
                return "坚果牌 (Nuts / Near-Nuts)";
            } else if (equity >= 0.65) {
                return "强顶对 (Top Pair Top Kicker) 或两对+";
            } else if (equity >= 0.45) {
                return "中等成牌 (Medium Made Hand)";
            } else if (equity >= 0.25) {
                return "听牌 (Draw)";
            } else {
                return "弱牌/诈唬 (Weak / Bluff)";
            }
        }
        
        return null;
    }
    
    /**
     * 分析范围优势（V4.8新增 - V4.14修复：结合胜率和位置）
     */
    private String analyzeRangeAdvantage(PokerAIRequest request) {
        String pos = request.getPlayerPos() != null ? request.getPlayerPos() : request.getHeroPos();
        String street = request.getStreet();
        String board = request.getBoard();
        Double equity = request.getPlayerEquity() != null ? request.getPlayerEquity() : request.getHeroEquity();
        
        // 判断是否是翻前3-bet玩家
        List<PokerAIRequest.ActionHistory> history = request.getActionsHistory();
        boolean is3Bettor = false;
        if (history != null) {
            for (PokerAIRequest.ActionHistory action : history) {
                if ("preflop".equalsIgnoreCase(action.getStreet()) && 
                    "raise".equalsIgnoreCase(action.getAction())) {
                    is3Bettor = true;
                    break;
                }
            }
        }
        
        // Postflop: 判断牌面是否有利于3-bet玩家
        if (!"preflop".equalsIgnoreCase(street) && board != null) {
            if (is3Bettor) {
                // 简化判断：高牌面（A/K/Q）有利于3-bet玩家
                if (board.contains("A") || board.contains("K")) {
                    return "高 (作为翻前3-bet玩家，你在Axx/Kxx干燥牌面有巨大优势)";
                } else if (board.contains("J") || board.contains("Q")) {
                    return "中等 (作为3-bet玩家，在此牌面有一定优势)";
                } else {
                    return "低 (低牌面对3-bet玩家不利，需谨慎)";
                }
            }
        }
        
        // Preflop: 结合胜率和位置判断
        if ("preflop".equalsIgnoreCase(street)) {
            // 早期位置（UTG/MP）：需要更强的牌
            if ("UTG".equals(pos) || "MP".equals(pos)) {
                if (equity != null && equity < 0.35) {
                    return "弱 (早期位置+低胜率)";
                } else if (equity != null && equity >= 0.45) {
                    return "中等 (早期位置但牌力尚可)";
                } else {
                    return "弱-中等 (早期位置)";
                }
            }
            // 中期位置（CO）
            else if ("CO".equals(pos)) {
                if (equity != null && equity >= 0.40) {
                    return "中高 (后期位置+可玩牌力)";
                } else {
                    return "中等 (后期位置)";
                }
            }
            // 最佳位置（BTN）
            else if ("BTN".equals(pos)) {
                return "高 (位置优势)";
            }
            // 盲注位置
            else if ("SB".equals(pos) || "BB".equals(pos)) {
                return "低 (位置劣势)";
            }
        }
        
        return "中等";
    }

    /**
     * 翻译位置名称
     */
    /**
     * 对手数量（精简）
     */
    /**
     * 计算在局对手数量（V4.8新增 - V4.14修复：Preflop时统计所有未行动玩家）
     */
    private int calculateOpponentCount(PokerAIRequest request) {
        if (request.getActionsBrief() == null) {
            // 如果没有actionsBrief，尝试使用playersAlive字段
            if (request.getPlayersAlive() != null && request.getPlayersAlive() > 0) {
                // 对手数 = 总玩家数 - 1（自己）
                return Math.max(1, request.getPlayersAlive() - 1);
            }
            return 1; // 默认至少1个对手
        }

        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> players = JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null || players.isEmpty()) {
                return 1;
            }

            String street = request.getStreet();
            String currentPlayerPos = request.getPlayerPos() != null ? request.getPlayerPos() : request.getHeroPos();
            int activeCount = 0;
            
            // Preflop特殊处理：需要统计所有未弃牌的玩家（包括还没行动的）
            if ("preflop".equalsIgnoreCase(street)) {
                // 定义位置顺序（Preflop：UTG先行动，BB最后）
                String[] preflopOrder = {"UTG", "MP", "CO", "BTN", "SB", "BB"};
                int currentPosIndex = -1;
                for (int i = 0; i < preflopOrder.length; i++) {
                    if (preflopOrder[i].equals(currentPlayerPos)) {
                        currentPosIndex = i;
                        break;
                    }
                }
                
                for (Map<String, Object> player : players) {
                    String status = (String)player.get("status");
                    Object isHeroObj = player.get("isHero");
                    boolean isHero = isHeroObj != null && (Boolean)isHeroObj;
                    String pos = (String)player.get("pos");
                    
                    if (!isHero && !"弃牌".equals(status)) {
                        // 统计所有未弃牌的对手（包括还没行动的）
                        activeCount++;
                    }
                }
            } else {
                // Postflop：只统计"在局"的玩家
                for (Map<String, Object> player : players) {
                    String status = (String)player.get("status");
                    Object isHeroObj = player.get("isHero");
                    boolean isHero = isHeroObj != null && (Boolean)isHeroObj;
                    
                    if (!isHero && "在局".equals(status)) {
                        activeCount++;
                    }
                }
            }
            
            return Math.max(1, activeCount); // 至少1个对手
        } catch (Exception e) {
            logger.debug("Failed to parse players info", e);
            return 1;
        }
    }
    
    /**
     * 提取在局对手的位置列表（V4.10新增 - V4.14修复：Preflop时提取所有未弃牌玩家）
     */
    private String extractOpponentPositions(PokerAIRequest request) {
        if (request.getActionsBrief() == null) {
            return null;
        }

        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> players = JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null || players.isEmpty()) {
                return null;
            }

            String street = request.getStreet();
            List<String> opponentPos = new ArrayList<>();
            
            for (Map<String, Object> player : players) {
                String status = (String)player.get("status");
                Object isHeroObj = player.get("isHero");
                boolean isHero = isHeroObj != null && (Boolean)isHeroObj;
                String pos = (String)player.get("pos");
                
                if (isHero || pos == null) {
                    continue;
                }
                
                // Preflop：提取所有未弃牌的玩家
                if ("preflop".equalsIgnoreCase(street)) {
                    if (!"弃牌".equals(status)) {
                        opponentPos.add(pos);
                    }
                } else {
                    // Postflop：只提取"在局"的玩家
                    if ("在局".equals(status)) {
                        opponentPos.add(pos);
                    }
                }
            }
            return opponentPos.isEmpty() ? null : String.join(",", opponentPos);
        } catch (Exception e) {
            logger.debug("Failed to extract opponent positions", e);
            return null;
        }
    }
    
    /**
     * 显示对手筹码列表（V4.16新增）
     */
    private void appendOpponentStacks(StringBuilder p, PokerAIRequest request) {
        if (request.getActionsBrief() == null) {
            return;
        }
        
        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> players = JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null || players.isEmpty()) {
                return;
            }
            
            String street = request.getStreet();
            Double bigBlind = request.getBigBlind();
            p.append("\n对手筹码：\n");
            
            for (Map<String, Object> player : players) {
                Object isHeroObj = player.get("isHero");
                boolean isHero = isHeroObj != null && (Boolean)isHeroObj;
                if (isHero) {
                    continue; // 跳过自己
                }
                
                String pos = (String)player.get("pos");
                String status = (String)player.get("status");
                Object stackObj = player.get("stack");
                
                if (pos == null) {
                    continue;
                }
                
                // Preflop：显示所有未弃牌的对手
                // Postflop：只显示在局的对手
                boolean shouldDisplay = false;
                if ("preflop".equalsIgnoreCase(street)) {
                    shouldDisplay = !"弃牌".equals(status);
                } else {
                    shouldDisplay = "在局".equals(status);
                }
                
                if (shouldDisplay && stackObj != null) {
                    double stack = 0;
                    if (stackObj instanceof Integer) {
                        stack = ((Integer)stackObj).doubleValue();
                    } else if (stackObj instanceof Double) {
                        stack = (Double)stackObj;
                    }
                    
                    p.append("  ").append(pos).append(": ").append(String.format("%.0f", stack));
                    
                    // 标注状态
                    if ("弃牌".equals(status)) {
                        p.append(" [已弃牌]");
                    } else if ("全押".equals(status)) {
                        p.append(" [全押]");
                    }
                    p.append("\n");
                }
            }
        } catch (Exception e) {
            logger.debug("Failed to append opponent stacks", e);
        }
    }
    
    private void appendOpponentCount(StringBuilder p, PokerAIRequest request) {
        int activeCount = calculateOpponentCount(request);
        p.append("在局对手：").append(activeCount).append("人");
    }
    
    /**
     * 添加对手信息（V4.8新增 - 简化版本）
     */
    private void appendOpponentInfo(StringBuilder p, PokerAIRequest request) {
        // 简化版本：基于行动历史推测对手类型
        // TODO: 未来可以从数据库中获取对手的VPIP/PFR等统计数据
        
        List<PokerAIRequest.ActionHistory> history = request.getActionsHistory();
        if (history == null || history.isEmpty()) {
            return; // 如果没有行动历史，不显示对手信息
        }
        
        p.append("=== 对手信息 ===\n");
        p.append("翻牌前行动历史: ");
        
        // 简化显示翻牌前行动
        StringBuilder preflopActions = new StringBuilder();
        for (PokerAIRequest.ActionHistory action : history) {
            if ("preflop".equalsIgnoreCase(action.getStreet())) {
                String pos = translatePosition(action.getPosition());
                String act = translateAction(action.getAction(), action.getSize());
                preflopActions.append(pos).append(" ").append(act).append(", ");
            }
        }
        if (preflopActions.length() > 2) {
            preflopActions.setLength(preflopActions.length() - 2); // 移除最后的", "
        }
        p.append(preflopActions).append("\n");
        
        // 翻牌后行动历史（如果有）
        if (!"preflop".equalsIgnoreCase(request.getStreet())) {
            String currentStreet = translateStreet(request.getStreet());
            p.append(currentStreet).append("行动历史: ");
            
            StringBuilder postflopActions = new StringBuilder();
            for (PokerAIRequest.ActionHistory action : history) {
                if (request.getStreet().equalsIgnoreCase(action.getStreet())) {
                    String pos = translatePosition(action.getPosition());
                    String act = translateAction(action.getAction(), action.getSize());
                    postflopActions.append(pos).append(" ").append(act).append(", ");
                }
            }
            if (postflopActions.length() > 2) {
                postflopActions.setLength(postflopActions.length() - 2);
            }
            p.append(postflopActions).append("\n");
        }
        
        // 简化的对手画像（基于行动历史推测）
        p.append("对手画像: 根据行动历史分析，请结合GTO和剥削性策略进行决策\n");
        p.append("\n");
    }

    /**
     * 关键行动历史（只显示最近几个重要行动）
     */
    /**
     * V4.12 行动历史增强：动词化 + 底池追踪 + 公共牌展示
     */
    private void appendActionsKey(StringBuilder p, List<PokerAIRequest.ActionHistory> actions) {
        if (actions == null || actions.isEmpty()) {
            return;
        }

        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet();
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }

        p.append("=== 行动历史 ===\n");
        
        // 使用标准街道顺序
        String[] streetOrder = {"preflop", "flop", "turn", "river"};
        double runningPot = 0.0;  // 追踪底池变化
        
        for (String street : streetOrder) {
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty()) {
                continue;
            }
            
            // 街道标题（纯英文，首字母大写）
            p.append(capitalizeFirstLetter(street));
            
            // Postflop显示底池（如果有）
            if (!"preflop".equals(street) && runningPot > 0) {
                p.append(String.format(", pot: %.0f", runningPot));
            }
            p.append(":\n");
            
            // 每个行动独立一行，使用缩进
            for (PokerAIRequest.ActionHistory action : streetActions) {
                p.append("  ");  // 两个空格缩进
                
                // 位置（纯英文大写）
                p.append(action.getPosition().toUpperCase());
                p.append(" ");
                
                // 动词化行动
                String verb = getActionVerb(action.getAction());
                p.append(verb);
                
                // 行动金额（如果有）
                if (action.getSize() != null && !action.getSize().isEmpty()) {
                    try {
                        // V4.18：去除$等符号
                        String sizeStr = action.getSize().replace("$", "").replace("￥", "").trim();
                        double actionSize = Double.parseDouble(sizeStr);
                        p.append(String.format(" %.0f", actionSize));
                        
                        // 更新底池（只统计投入的筹码）
                        if (isMoneyAction(action.getAction())) {
                            runningPot += actionSize;
                        }
                    } catch (NumberFormatException e) {
                        // 如果解析失败，去除符号后直接显示
                        String sizeStr = action.getSize().replace("$", "").replace("￥", "").trim();
                        p.append(" ").append(sizeStr);
                    }
                }
                
                // 盲注标注
                if ("small_blind".equals(action.getAction()) || "big_blind".equals(action.getAction())) {
                    p.append(" (blind)");
                }
                
                // 显示当前底池（对于投钱的行动）
                if (isMoneyAction(action.getAction()) && runningPot > 0) {
                    p.append(String.format(" → pot: %.0f", runningPot));
                }
                
                p.append("\n");
            }
        }
        p.append("\n");
    }
    
    /**
     * V4.12辅助：将行动转换为动词形式
     */
    private String getActionVerb(String action) {
        if (action == null) return "";
        
        switch (action.toLowerCase()) {
            case "small_blind":
            case "big_blind":
                return "posts";
            case "call":
                return "calls";
            case "raise":
            case "3bet":
            case "4bet":
                return "raises";
            case "bet":
            case "open":
            case "donk":
            case "probe":
                return "bets";
            case "check":
                return "checks";
            case "fold":
                return "folds";
            case "all-in":
            case "allin":
                return "goes all-in";
            default:
                return action;  // 保持原样
        }
    }
    
    /**
     * V4.12辅助：判断是否是投钱行动
     */
    private boolean isMoneyAction(String action) {
        if (action == null) return false;
        
        String lowerAction = action.toLowerCase();
        return lowerAction.contains("blind") || 
               lowerAction.contains("call") || 
               lowerAction.contains("raise") || 
               lowerAction.contains("bet") || 
               lowerAction.contains("3bet") || 
               lowerAction.contains("4bet") ||
               lowerAction.contains("open") ||
               lowerAction.contains("donk") ||
               lowerAction.contains("probe") ||
               lowerAction.contains("all");
    }
    
    /**
     * 辅助方法：首字母大写
     */
    private String capitalizeFirstLetter(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }

    /**
     * 决策要求（V3.1 - 添加尺寸参考，但不强制）
     */
    private void appendDecisionRequest(StringBuilder p, PokerAIRequest request) {
        String street = request.getStreet();
        
        // 尺寸参考（V4.19优化：更详细的指导）
        p.append("=== 参考尺寸 ===\n");
        if ("preflop".equalsIgnoreCase(street)) {
            int limperCount = countLimpers(request);
            Double bigBlind = request.getBigBlind();
            Double pot = request.getPot();
            Double playerStack = request.getStacks();
            Double spr = request.getSpr();
            if (bigBlind == null) bigBlind = 200.0;
            
            // 自动计算SPR
            if (spr == null && playerStack != null && pot != null && pot > 0) {
                spr = playerStack / pot;
            }
            
            if (limperCount > 0) {
                // 有跛入者：显示隔离加注
                double adjustedRaise = bigBlind * 2.5 + (limperCount * bigBlind);
                p.append("隔离加注：").append(String.format("%.0f", adjustedRaise)).append("\n");
            } else {
                // 无跛入者：显示标准open raise
                double openRaise = bigBlind * 2.5;
                p.append("标准Open：").append(String.format("%.0f", openRaise)).append("\n");
            }
            
            // 反加注尺寸参考
            double threeBetSize = bigBlind * 8.0; // 约4倍open raise
            p.append("反加注尺寸：").append(String.format("%.0f", threeBetSize)).append("\n");
            
            // 根据SPR给出建议
            if (spr != null) {
                if (spr < 4) {
                    p.append("注意：低SPR，顶对可全下\n");
                } else if (spr > 10) {
                    p.append("注意：高SPR，边缘牌需谨慎\n");
                }
            }
            p.append("\n");
        } else {
            // Postflop尺寸参考
            Double pot = request.getPot();
            if (pot != null) {
                p.append("小尺寸(1/3底池)：").append(String.format("%.0f", pot * 0.33)).append("\n");
                p.append("标准尺寸(2/3底池)：").append(String.format("%.0f", pot * 0.67)).append("\n");
                p.append("大尺寸(1倍底池)：").append(String.format("%.0f", pot)).append("\n\n");
            } else {
                p.append("C-bet：33-125\n\n");
            }
        }
        
        // 决策选项（V4.19优化：修复翻前逻辑）
        p.append("=== 决策 ===\n");
        
        // 翻前首个行动：只能fold或raise
        if ("preflop".equalsIgnoreCase(street) && 
            (request.getAmountToCall() == null || request.getAmountToCall() == 0)) {
            p.append("可选：fold, raise\n");
        }
        // 面对下注/加注：可以fold, call, raise
        else if (request.getAmountToCall() != null && request.getAmountToCall() > 0) {
            p.append("可选：fold, call, raise\n");
        }
        // Postflop可以check（无需跟注）
        else if (request.getCanCheck() != null && request.getCanCheck()) {
            p.append("可选：check, bet, fold\n");
        }
        // 其他情况
        else {
            p.append("可选：fold, raise\n");
        }
        
        // 根据数学依据给出建议
        Double playerEquity = request.getPlayerEquity() != null ? request.getPlayerEquity() : request.getHeroEquity();
        if ("preflop".equalsIgnoreCase(street) && playerEquity != null) {
            String position = request.getPlayerPos() != null ? request.getPlayerPos() : request.getHeroPos();
            double gtoThreshold = getGTOEquityThreshold(position);
            if (playerEquity < gtoThreshold) {
                p.append("建议：fold（胜率不足GTO标准）\n");
            }
        }
        p.append("\n");
        
        // V4.35：Few-shot Examples（通俗易懂版本，使用标准牌面格式）
        p.append("=== Reasoning示例 (通俗专业风格) ===\n\n");
        p.append("✅ 示例1 (弱牌果断弃):\n");
        p.append("\"7h2d是很弱的起手牌，胜率26.1不到阈值35.0，对抗UTG加注范围{99+, ATs+}差太多，直接fold\"\n\n");
        
        p.append("✅ 示例2 (胜率够但仍要弃 - 关键场景):\n");
        p.append("\"3h2d胜率28.4虽然超过BB阈值25.0，但面对BTN加注范围{22+, A2s+, A9o+}牌力还是不行，加上SPR33筹码深，后续不好打，fold是最优选择\"\n\n");
        
        p.append("✅ 示例3 (强牌积极进攻):\n");
        p.append("\"AhKh同花大牌胜率64.8远超BTN阈值28.0，牌力强又有位置优势，raise可以压制对手范围，最大化收益\"\n\n");
        
        p.append("❌ 错误示例 (逻辑错误 - 绝对禁止):\n");
        p.append("\"3h2d弱起手牌胜率28.4低于阈值25.0\" ← 错误！28.4>25.0，逻辑相反了！\n\n");
        
        p.append("【提示】先确认数值大小，再描述！胜率高低、阈值比较别搞反了。\n\n");
        
        // V4.35：JSON返回格式（标准牌面格式）
        p.append("=== 返回格式 ===\n");
        p.append("返回JSON示例：\n");
        p.append("{\n");
        p.append("  \"action\": \"fold\",\n");
        p.append("  \"size\": \"0\",\n");
        p.append("  \"confidence\": \"0.95\",\n");
        p.append("  \"reasons\": \"7h2d很弱的起手牌，胜率26.1达不到GTO阈值35.0，对抗5人桌后续3-bet压力太大，直接弃牌\"\n");
        p.append("}\n\n");
        
        // V4.35：格式要求（标准牌面格式）
        p.append("【输出格式要求】\n");
        p.append("1. action：fold/check/call/bet/raise之一\n");
        p.append("2. size：纯数字，fold/check时填0\n");
        p.append("3. confidence：0到1之间的小数（如0.82表示82%信心）\n");
        p.append("4. reasons格式（50-70字，像和朋友聊牌一样自然）：\n");
        p.append("   [简述手牌] + [胜率和阈值比较] + [决策理由]\n");
        p.append("   好的示例: \"8d3h是弱起手牌，胜率28.4虽然超过阈值25.0但对抗BTN加注范围牌力不够，筹码深后续不好打，fold最优\"\n");
        p.append("5. reasons中数值不带单位（写26.1，不写26.1%）\n");
        p.append("6. **关键**：胜率和阈值比较别搞反！28.4>25.0就是\"超过\"，不是\"低于\"\n");
        p.append("7. **牌面格式**：使用标准格式（如Jh3s、AhKh），不要自创格式\n");
        
        // V4.35: 简化language参数（zh/en）
        String language = request.getLanguage();
        if ("en".equalsIgnoreCase(language)) {
            p.append("8. **Output Language**: English. All reasoning must be in plain, professional English.\n\n");
        } else {
            p.append("8. **输出语言**：中文。reasoning要通俗易懂但保持专业性，像和朋友聊牌一样自然。\n\n");
        }
        
        p.append("根据GTO、数学依据和当前底牌，给出EV最大化决策。");
    }
    
    /**
     * Preflop尺寸参考
     */
    private void appendPreflopSizeReference(StringBuilder p, PokerAIRequest request) {
        Double bigBlind = request.getBigBlind();
        if (bigBlind == null) bigBlind = 200.0; // 默认
        
        // 统计前面有多少跟注者
        int limperCount = countLimpers(request);
        
        if (request.getFacingBet() != null && request.getFacingBet()) {
            // 面对加注
            p.append("3-bet参考：约为对手加注额的3倍\n");
        } else {
            // Open raise
            double baseRaise = bigBlind * 2.5;
            double adjustedRaise = baseRaise + (limperCount * bigBlind);
            
            p.append("Open raise参考：").append(String.format("%.0f", adjustedRaise));
            if (limperCount > 0) {
                p.append("（2.5BB + ").append(limperCount).append("个跟注者）");
            } else {
                p.append("（2.5-3.5BB）");
            }
            p.append("\n");
        }
    }
    
    /**
     * Postflop尺寸参考
     */
    private void appendPostflopSizeReference(StringBuilder p, PokerAIRequest request) {
        Double pot = request.getPot();
        if (pot == null) pot = 1000.0;
        
        Double equity = request.getPlayerEquity() != null ? request.getPlayerEquity() : request.getHeroEquity();
        String street = request.getStreet();
        
        if (request.getCanCheck() != null && request.getCanCheck()) {
            // 可以下注的场景
            p.append("C-bet参考：33%（小）、66%（标准）、125%（超额）底池\n");
            
            if (equity != null && equity > 0.75) {
                // 强牌
                if ("river".equalsIgnoreCase(street)) {
                    // River根据对手筹码深度和行为分析给出建议
                    Double playerStack = request.getStacks();
                    
                    // 即使playerStack为null，也尝试从其他信息推断
                    if (playerStack == null && pot != null) {
                        // 从对手行为推断筹码深度
                        int turnCalls = countStreetCalls(request, "turn");
                        if (turnCalls > 0) {
                            // 假设对手剩余筹码约为底池的1.4倍（基于多街道跟注的实际场景）
                            playerStack = pot * 1.4;
                        }
                    }
                    
                    // 分析对手行为
                    String opponentBehavior = analyzeOpponentBehavior(request);
                    
                    if (playerStack != null && pot != null) {
                        double opponentStack = playerStack * 0.7; // 估计对手筹码
                        double stackToPotRatio = opponentStack / pot;
                        
                        if (stackToPotRatio < 0.8) {
                            // 对手筹码 < 0.8倍底池，全压
                            p.append("强牌River：对手筹码少(<0.8倍底池)，考虑全压\n");
                        } else if (stackToPotRatio < 1.5) {
                            // 对手筹码 1-1.5倍底池，诱导性下注（重点优化）
                            double estimatedInvested = pot * 0.46; // 估算对手已投入（约46%底池）
                            p.append("【River价值最大化策略】\n");
                            p.append("对手筹码：").append(String.format("%.1f", stackToPotRatio)).append("倍底池\n");
                            p.append("对手已投入：约").append(String.format("%.0f", estimatedInvested)).append("（沉没成本高，弃牌成本大）\n");
                            if (opponentBehavior != null && !opponentBehavior.isEmpty()) {
                                p.append("对手行为：").append(opponentBehavior).append("\n");
                            }
                            p.append("\n最优策略：\n");
                            p.append("- 推荐尺寸：25-35%底池（约").append(String.format("%.0f", pot * 0.25)).append("-").append(String.format("%.0f", pot * 0.35)).append("）\n");
                            p.append("- 核心逻辑：让对手获得'合理价格'（底池赔率4:1-3:1），跟注率最高\n");
                            p.append("- 对比：40%+降低跟注率，50%+容易让对手弃牌（如本局）\n");
                            p.append("- 警告：避免'争取最大收益但吓退对手'的矛盾策略\n");
                            p.append("- 目标：诱导跟注，总收益=底池+跟注额，而非仅赢底池\n");
                            return; // 早返回，避免添加简化版
                        } else {
                            // 对手筹码 > 1.5倍底池，标准价值下注
                            p.append("强牌River：对手筹码深(>1.5倍底池)，可用50-66%底池提取价值\n");
                            return;
                        }
                    }
                    
                    // 如果走到这里，说明无法推断筹码，使用通用建议
                    p.append("强牌River：优先考虑25-35%底池诱导跟注（总收益最大），深筹码可用50-66%底池\n");
                } else {
                    p.append("强牌下注：66-100%底池提取价值\n");
                }
            }
        } else if (request.getFacingBet() != null && request.getFacingBet()) {
            // 面对下注
            p.append("加注参考：2.5-3x对手下注额（价值），或转为全压\n");
        }
    }
    
    /**
     * 统计指定街道的跟注次数
     */
    private int countStreetCalls(PokerAIRequest request, String targetStreet) {
        if (request.getActionsHistory() == null) return 0;
        
        int count = 0;
        for (PokerAIRequest.ActionHistory action : request.getActionsHistory()) {
            if (targetStreet.equalsIgnoreCase(action.getStreet()) && 
                "call".equalsIgnoreCase(action.getAction())) {
                count++;
            }
        }
        return count;
    }
    
    /**
     * 统计前面有多少跟注者
     */
    private int countLimpers(PokerAIRequest request) {
        if (request.getActionsHistory() == null) return 0;
        
        int count = 0;
        for (PokerAIRequest.ActionHistory action : request.getActionsHistory()) {
            if ("preflop".equalsIgnoreCase(action.getStreet()) && 
                "call".equalsIgnoreCase(action.getAction())) {
                count++;
            }
        }
        return count;
    }
    
    /**
     * 分析对手行为特征（用于River诱导跟注策略）
     */
    private String analyzeOpponentBehavior(PokerAIRequest request) {
        if (request.getActionsHistory() == null || request.getActionsHistory().isEmpty()) {
            return null;
        }
        
        List<String> behaviors = new ArrayList<>();
        
        // 统计对手在各街道的跟注次数
        int flopCalls = 0;
        int turnCalls = 0;
        
        for (PokerAIRequest.ActionHistory action : request.getActionsHistory()) {
            String street = action.getStreet();
            String act = action.getAction();
            
            if ("call".equalsIgnoreCase(act)) {
                if ("flop".equalsIgnoreCase(street)) {
                    flopCalls++;
                } else if ("turn".equalsIgnoreCase(street)) {
                    turnCalls++;
                }
            }
        }
        
        // 如果对手在Flop和Turn都跟注，说明有一定牌力且不易弃牌
        if (flopCalls > 0 && turnCalls > 0) {
            behaviors.add("多街道跟注，显示有牌力且不易弃牌");
        } else if (turnCalls > 0) {
            behaviors.add("Turn跟注，牌力中等偏上");
        }
        
        return behaviors.isEmpty() ? null : String.join("；", behaviors);
    }

    private String translatePosition(String pos) {
        if (pos == null) return "未知";
        switch (pos.toUpperCase()) {
            case "SB": return "小盲(SB)";
            case "BB": return "大盲(BB)";
            case "UTG": return "枪口(UTG)";
            case "MP": return "中位(MP)";
            case "CO": return "关煞(CO)";
            case "BTN": return "按钮(BTN)";
            default: return pos;
        }
    }
    
    /**
     * 输出所有玩家信息
     */
    private void appendAllPlayers(StringBuilder p, PokerAIRequest request) {
        if (request.getActionsBrief() == null) {
            return;
        }

        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> players = JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null || players.isEmpty()) {
                return;
            }

            p.append("【玩家信息】\n");
            for (Map<String, Object> player : players) {
                String pos = (String)player.get("pos");
                Number stackNum = (Number)player.get("stack");
                String status = (String)player.get("status");
                Object isHeroObj = player.get("isHero");
                boolean isHero = isHeroObj != null && (Boolean)isHeroObj;

                p.append("- ").append(translatePosition(pos)).append(": ");
                if (stackNum != null) {
                    p.append("$").append(String.format("%.0f", stackNum.doubleValue()));
                }
                if ("已弃牌".equals(status)) {
                    p.append(" [已弃牌]");
                } else if ("在局".equals(status)) {
                    p.append(" [在局]");
                }
                if (isHero) {
                    p.append(" ← Hero");
                }
                p.append("\n");
            }
            p.append("\n");
        } catch (Exception e) {
            logger.debug("Failed to parse players info", e);
        }
    }

    /**
     * 输出详细的行动历史
     */
    private void appendActionsDetailed(StringBuilder p, List<PokerAIRequest.ActionHistory> actions, String currentStreet) {
        if (actions == null || actions.isEmpty()) {
            return;
        }

        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet();
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }

        p.append("【行动历史】\n");
        String[] streetOrder = {"preflop", "flop", "turn", "river"};

        for (String street : streetOrder) {
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty()) {
                continue;
            }

            p.append(translateStreet(street)).append(":\n");

            for (PokerAIRequest.ActionHistory action : streetActions) {
                String pos = action.getPosition();
                String act = action.getAction();
                String size = action.getSize();

                p.append("  - ").append(translatePosition(pos)).append(": ");
                p.append(translateAction(act, size));
                p.append("\n");
            }
        }
        p.append("\n");
    }

    /**
     * 翻译行动
     */
    private String translateAction(String action, String size) {
        if (action == null) return "未知";
        
        // 移除size中的单位符号
        String cleanSize = size;
        if (size != null && !size.isEmpty()) {
            cleanSize = size.replace("$", "").replace("¥", "").trim();
        }

        String lower = action.toLowerCase();
        if (lower.contains("fold")) {
            return "弃牌(fold)";
        } else if (lower.contains("check")) {
            return "过牌(check)";
        } else if (lower.contains("call")) {
            if (cleanSize != null && !cleanSize.isEmpty()) {
                return String.format("跟注(call) %s", cleanSize);
            }
            return "跟注(call)";
        } else if (lower.contains("raise")) {
            if (cleanSize != null && !cleanSize.isEmpty()) {
                return String.format("加注(raise) 到 %s", cleanSize);
            }
            return "加注(raise)";
        } else if (lower.contains("bet")) {
            if (cleanSize != null && !cleanSize.isEmpty()) {
                return String.format("下注(bet) %s", cleanSize);
            }
            return "下注(bet)";
        } else if (lower.contains("small_blind")) {
            if (cleanSize != null && !cleanSize.isEmpty()) {
                return String.format("下小盲 %s", cleanSize);
            }
            return "下小盲";
        } else if (lower.contains("big_blind")) {
            if (cleanSize != null && !cleanSize.isEmpty()) {
                return String.format("下大盲 %s", cleanSize);
            }
            return "下大盲";
        } else {
            return action;
        }
    }

    /**
     * 详细的数学分析
     */
    private void appendMathAnalysisDetailed(StringBuilder p, PokerAIRequest request) {
        p.append("【数学分析】\n");

        // 权益
        if (request.getHeroEquity() != null) {
            p.append("- Hero胜率: ").append(String.format("%.1f%%", request.getHeroEquity() * 100)).append("\n");
        }

        // 底池赔率和EV
        if (request.getPotOddsNote() != null) {
            String note = request.getPotOddsNote();
            if (note.contains("需跟注$") && note.contains("底池$")) {
                try {
                    int toCallStart = note.indexOf("需跟注$") + 4;
                    int toCallEnd = note.indexOf("，", toCallStart);
                    int potStart = note.indexOf("底池$") + 3;
                    int potEnd = note.indexOf("（", potStart);

                    if (toCallEnd > toCallStart && potEnd > potStart) {
                        double toCall = Double.parseDouble(note.substring(toCallStart, toCallEnd));
                        double pot = Double.parseDouble(note.substring(potStart, potEnd));
                        double equity = request.getHeroEquity() != null ? request.getHeroEquity() : 0.5;

                        double potOdds = toCall / (pot + toCall);
                        double requiredEquity = potOdds;
                        double ev = (equity * (pot + toCall)) - ((1 - equity) * toCall);

                        p.append("- 底池赔率: ").append(String.format("%.1f%%", potOdds * 100)).append("\n");
                        p.append("- 所需胜率: ").append(String.format("%.1f%%", requiredEquity * 100)).append("\n");
                        p.append("- 期望值(EV): ");
                        if (ev > 0) {
                            p.append("+$").append(String.format("%.0f", ev)).append(" (正EV，有利可图)\n");
                        } else {
                            p.append("-$").append(String.format("%.0f", Math.abs(ev))).append(" (负EV，不利)\n");
                        }
                    }
                } catch (Exception ignored) {
                }
            } else if (note.contains("无需跟注")) {
                p.append("- 当前无需跟注，可以免费看牌或主动下注\n");
            }
        }

        // SPR分析
        if (request.getSpr() != null) {
            double spr = request.getSpr();
            p.append("- SPR(筹码底池比): ").append(String.format("%.1f", spr));
            if (spr < 3.0) {
                p.append(" (浅筹码，适合全压)");
            } else if (spr > 10.0) {
                p.append(" (深筹码，需谨慎)");
            } else {
                p.append(" (中等筹码)");
            }
            p.append("\n");
        }

        p.append("\n");
    }

    /**
     * 数学分析（优化精简版 - 仅关键指标）
     */
    private void appendMathAnalysisOptimized(StringBuilder p, PokerAIRequest request) {
        p.append("【数学】");
        
        // 权益
        if (request.getHeroEquity() != null) {
            p.append("权益:").append(String.format("%.1f%%", request.getHeroEquity() * 100));
        }
        
        // EV和赔率
        if (request.getPotOddsNote() != null) {
            String note = request.getPotOddsNote();
            if (note.contains("需跟注$") && note.contains("底池$")) {
                try {
                    int toCallStart = note.indexOf("需跟注$") + 4;
                    int toCallEnd = note.indexOf("，", toCallStart);
                    int potStart = note.indexOf("底池$") + 3;
                    int potEnd = note.indexOf("（", potStart);
                    
                    if (toCallEnd > toCallStart && potEnd > potStart) {
                        double toCall = Double.parseDouble(note.substring(toCallStart, toCallEnd));
                        double pot = Double.parseDouble(note.substring(potStart, potEnd));
                        double equity = request.getHeroEquity() != null ? request.getHeroEquity() : 0.5;
                        
                        double potOdds = toCall / (pot + toCall);
                        double ev = (equity * (pot + toCall)) - ((1 - equity) * toCall);
                        
                        p.append(" 赔率:").append(String.format("%.1f%%", potOdds * 100));
                        p.append(" EV:").append(ev > 0 ? "+" : "").append(String.format("%.0f", ev));
                    }
                } catch (Exception ignored) {
                }
            }
        }
        p.append("\n");
    }
    
    /**
     * 对手信息（仅活跃玩家）
     */
    private void appendOpponentsOptimized(StringBuilder p, PokerAIRequest request) {
        if (request.getActionsBrief() == null) {
            return;
        }
        
        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> players = JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null) {
                return;
            }
            
            StringBuilder activePlayers = new StringBuilder();
            for (Map<String, Object> player : players) {
                Object isHeroObj = player.get("isHero");
                boolean isHero = isHeroObj != null && (Boolean)isHeroObj;
                String status = (String)player.get("status");
                
                // 只显示在局的非Hero玩家
                if (!isHero && "在局".equals(status)) {
                    String pos = (String)player.get("pos");
                    if (activePlayers.length() > 0) {
                        activePlayers.append(" ");
                    }
                    activePlayers.append(pos);
                }
            }
            
            if (activePlayers.length() > 0) {
                p.append("【对手】").append(activePlayers).append("\n");
            }
        } catch (Exception ignored) {
        }
    }
    
    /**
     * 行动历史（超精简格式）
     */
    private void appendActionsOptimized(StringBuilder p, List<PokerAIRequest.ActionHistory> actions, String currentStreet) {
        if (actions == null || actions.isEmpty()) {
            return;
        }
        
        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet();
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }
        
        p.append("【行动】");
        String[] streetOrder = {"preflop", "flop", "turn", "river"};
        boolean first = true;
        
        for (String street : streetOrder) {
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty()) {
                continue;
            }
            
            if (!first) {
                p.append(" | ");
            }
            first = false;
            
            // 街道简写
            String streetShort = street.equals("preflop") ? "Pre" : 
                                street.equals("flop") ? "Flop" : 
                                street.equals("turn") ? "Turn" : "River";
            p.append(streetShort).append(":");
            
            // 行动序列（超精简）
            for (int i = 0; i < streetActions.size(); i++) {
                if (i > 0) p.append(" ");
                PokerAIRequest.ActionHistory action = streetActions.get(i);
                String pos = action.getPosition().replaceAll("[^A-Z]", "");
                String act = action.getAction();
                String actShort = act.startsWith("fold") ? "F" :
                                 act.startsWith("call") ? "C" :
                                 act.startsWith("raise") ? "R" :
                                 act.startsWith("check") ? "X" :
                                 act.startsWith("bet") ? "B" : act.substring(0, 1);
                p.append(pos).append(actShort);
            }
        }
        p.append("\n");
    }

    /**
     * 数学分析（优化版 - 简洁高效）
     */
    private void appendMathAnalysisChinese(StringBuilder p, PokerAIRequest request) {
        p.append("数学:\n");

        // 1. 权益和胜率
        if (request.getHeroEquity() != null) {
            double equity = request.getHeroEquity();
            p.append("  权益: ").append(String.format("%.1f%%", equity * 100));
            p.append(" | 胜率: ").append(String.format("%.1f%%", equity * 0.95 * 100));
            p.append(" | 平局: ").append(String.format("%.1f%%", equity * 0.05 * 100)).append("\n");
        }

        // 2. 底池赔率和EV分析（紧凑格式）
        if (request.getPotOddsNote() != null) {
            String note = request.getPotOddsNote();

            if (note.contains("需跟注$") && note.contains("底池$")) {
                try {
                    int toCallStart = note.indexOf("需跟注$") + 4;
                    int toCallEnd = note.indexOf("，", toCallStart);
                    int potStart = note.indexOf("底池$") + 3;
                    int potEnd = note.indexOf("（", potStart);

                    if (toCallEnd > toCallStart && potEnd > potStart) {
                        double toCall = Double.parseDouble(note.substring(toCallStart, toCallEnd));
                        double pot = Double.parseDouble(note.substring(potStart, potEnd));
                        double equity = request.getHeroEquity() != null ? request.getHeroEquity() : 0.5;

                        // 底池赔率和所需权益
                        double potOdds = toCall / (pot + toCall);

                        // 只显示数字，不带单位
                        p.append("  跟注: ").append(String.format("%.0f", toCall));
                        p.append(" | 赔率: ").append(String.format("%.1f%%", potOdds * 100));

                        // 期望值(EV) - 只显示数字
                        double ev = (equity * (pot + toCall)) - ((1 - equity) * toCall);
                        p.append(" | EV: ");
                        if (ev > 0) {
                            p.append("+").append(String.format("%.0f", ev));
                        } else {
                            p.append(String.format("%.0f", ev));
                        }
                        p.append("\n");

                        // 隐含赔率 - 只显示数字
                        if (request.getStacksBb() != null) {
                            double stack = request.getStacksBb() * 200;
                            double impliedOdds = (pot + stack * 0.3) / toCall;
                            p.append("  隐含赔率: ").append(String.format("%.1f", impliedOdds));

                            // MDF（最小防守频率）
                            double mdf = pot / (pot + toCall);
                            p.append(" | MDF: ").append(String.format("%.1f%%", mdf * 100));
                            p.append("\n");
                        }

                    }
                } catch (Exception e) {
                    p.append("  ").append(note).append("\n");
                }
            } else if (note.contains("无需跟注")) {
                // 当无需跟注时（可以check或bet）
                p.append("  ").append(note).append("\n");

                // 5. 下注时的弃牌权益(Fold Equity)和Alpha
                if (request.getPotBb() != null && request.getHeroEquity() != null) {
                    double pot = request.getPotBb() * 200; // 转换为美分
                    double equity = request.getHeroEquity();

                    // 假设下注0.66pot
                    double betSize = pot * 0.66;

                    // Alpha: 对手需要弃牌的频率才能让我们的下注profitable
                    double alpha = betSize / (pot + betSize);
                    p.append("  Alpha(α): ").append(String.format("%.1f%%", alpha * 100));
                    p.append(" (对手需要弃牌率使下注盈利)\n");

                    // 弃牌权益估算（假设对手40%弃牌率）- 只显示数字
                    double foldRate = 0.4; // 估算值
                    double foldEquity = foldRate * pot;
                    p.append("  弃牌权益(Fold Equity): ≈").append(String.format("%.0f", foldEquity));
                    p.append(" (假设对手").append(String.format("%.0f%%", foldRate * 100)).append("弃牌)\n");

                    // 下注EV估算 - 只显示数字
                    double betEV = (foldRate * pot) + ((1 - foldRate) * (equity * (pot + betSize * 2) - betSize));
                    p.append("  下注EV: ≈").append(String.format("%.0f", betEV));
                    if (betEV > 0) {
                        p.append(" (下注盈利)\n");
                    } else {
                        p.append("\n");
                    }
                }
            }
        }

        // 6. 手牌强度等级
        if (request.getHeroEquity() != null) {
            double equity = request.getHeroEquity();
            String strength = "";
            if (equity >= 0.80)
                strength = "坚果级(Nuts)";
            else if (equity >= 0.65)
                strength = "强牌(Strong)";
            else if (equity >= 0.50)
                strength = "中等偏强(Above Average)";
            else if (equity >= 0.35)
                strength = "中等(Average)";
            else if (equity >= 0.20)
                strength = "弱牌(Weak)";
            else
                strength = "极弱(Very Weak)";

            p.append("  手牌强度: ").append(strength).append("\n");
        }
    }

    /**
     * 对手信息（中文版 - 显示所有玩家）
     */
    private void appendOpponentsDetailChinese(StringBuilder p, PokerAIRequest request) {
        if (request.getActionsBrief() == null || request.getActionsBrief().isEmpty()) {
            p.append("暂无玩家数据\n");
            return;
        }

        try {
            @SuppressWarnings("unchecked") List<Map<String, Object>> players =
                JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null) {
                p.append("暂无玩家数据\n");
                return;
            }

            // 获取BB值用于筹码转换
            double bbValue = 200.0; // 默认BB=200美分

            // 尝试从Hero的信息反推BB值
            if (request.getStacksBb() != null && request.getStacksBb() > 0) {
                for (Map<String, Object> player : players) {
                    Object isHeroObj = player.get("isHero");
                    if (isHeroObj != null && (Boolean)isHeroObj) {
                        Object stackObj = player.get("stack");
                        if (stackObj != null) {
                            double stackCents = ((Number)stackObj).doubleValue();
                            if (stackCents > 0) {
                                bbValue = stackCents / request.getStacksBb();
                                break;
                            }
                        }
                    }
                }
            }

            // 显示所有玩家（Hero除外）
            int count = 0;
            for (Map<String, Object> player : players) {
                Object isHeroObj = player.get("isHero");
                boolean isHero = isHeroObj != null && (Boolean)isHeroObj;
                if (isHero)
                    continue; // 跳过Hero

                String pos = (String)player.get("pos");
                Object stackObj = player.get("stack");
                if (stackObj == null)
                    continue; // 跳过没有筹码信息的玩家

                double stackCents = ((Number)stackObj).doubleValue();
                String status = (String)player.get("status");

                p.append(pos).append(": ").append(String.format("%.0f", stackCents));

                // 显示状态（如果已弃牌）
                if ("已弃牌".equals(status)) {
                    p.append(" (已弃牌)");
                } else {
                    // 获取最后行动
                    String lastAction = getLastAction(request.getActionsHistory(), pos, request.getStreet());
                    if (lastAction != null) {
                        p.append(" (").append(lastAction).append(")");
                    }
                }

                p.append("\n");
                count++;
            }

            if (count == 0) {
                p.append("无其他玩家\n");
            }

        } catch (Exception e) {
            p.append("玩家数据解析错误: ").append(e.getMessage()).append("\n");
        }
    }

    /**
     * 行动历史（中文标签+英文术语+筹码信息）
     */
    private void appendActionsChineseLabel(StringBuilder p, List<PokerAIRequest.ActionHistory> actions,
        String currentStreet) {
        if (actions == null || actions.isEmpty()) {
            p.append("暂无行动\n");
            return;
        }

        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet() != null ? action.getStreet() : currentStreet;
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }

        // 输出
        String[] streetOrder = {"preflop", "flop", "turn", "river"};
        String[] streetChinese = {"翻牌前", "翻牌圈", "转牌圈", "河牌圈"};

        for (int i = 0; i < streetOrder.length; i++) {
            String street = streetOrder[i];
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty())
                continue;

            p.append(streetChinese[i]).append(":\n");

            // 详细模式：每个行动一行，包含筹码信息
            for (PokerAIRequest.ActionHistory action : streetActions) {
                String pos = action.getPosition().replaceAll("[^A-Z]", "");
                String act = action.getAction();
                String size = action.getSize();

                p.append("  ").append(pos).append(":").append(act);

                // 添加下注金额
                if (size != null && !size.isEmpty()) {
                    p.append(" ").append(size);
                }

                p.append("\n");
            }
        }
    }

    /**
     * 完整数学分析
     */
    private void appendMathAnalysis(StringBuilder p, PokerAIRequest request) {
        p.append("Math:\n");

        // Equity（权益）
        if (request.getHeroEquity() != null) {
            p.append("  Equity: ").append(String.format("%.1f%%", request.getHeroEquity() * 100)).append("\n");
        }

        // 解析底池赔率信息
        if (request.getPotOddsNote() != null) {
            String note = request.getPotOddsNote();

            // 提取需跟注金额和底池金额
            if (note.contains("需跟注$") && note.contains("底池$")) {
                try {
                    int toCallStart = note.indexOf("需跟注$") + 4;
                    int toCallEnd = note.indexOf("，", toCallStart);
                    int potStart = note.indexOf("底池$") + 3;
                    int potEnd = note.indexOf("（", potStart);

                    if (toCallEnd > toCallStart && potEnd > potStart) {
                        double toCall = Double.parseDouble(note.substring(toCallStart, toCallEnd));
                        double pot = Double.parseDouble(note.substring(potStart, potEnd));

                        // 计算底池赔率
                        double potOdds = toCall / (pot + toCall);
                        double requiredEquity = potOdds * 100;

                        // 计算期望值
                        double equity = request.getHeroEquity() != null ? request.getHeroEquity() : 0.5;
                        double ev = (equity * (pot + toCall)) - ((1 - equity) * toCall);

                        p.append("  To Call: ").append(String.format("%.0f", toCall)).append("\n");
                        p.append("  Pot Odds: ").append(String.format("%.1f%%", potOdds * 100));
                        p.append(" (need ").append(String.format("%.1f%%", requiredEquity)).append(" equity)\n");
                        p.append("  EV: ").append(String.format("%.0f", ev));
                        if (ev > 0) {
                            p.append(" (Profitable)\n");
                        } else {
                            p.append(" (Unprofitable)\n");
                        }
                    }
                } catch (Exception e) {
                    // 解析失败，使用原始信息
                    p.append("  ").append(note).append("\n");
                }
            } else {
                p.append("  ").append(note).append("\n");
            }
        }
    }

    /**
     * 对手详细信息（含筹码）
     */
    private void appendOpponentsDetailWithStack(StringBuilder p, PokerAIRequest request) {
        if (request.getActionsBrief() == null || request.getActionsBrief().isEmpty()) {
            p.append("No opponent data\n");
            return;
        }

        try {
            @SuppressWarnings("unchecked") List<Map<String, Object>> players =
                JsonUtil.fromJson(request.getActionsBrief(), List.class);
            if (players == null) {
                p.append("No opponent data\n");
                return;
            }

            // 只显示在局的对手
            int count = 0;
            for (Map<String, Object> player : players) {
                boolean isHero = (Boolean)player.get("isHero");
                String status = (String)player.get("status");

                if (!isHero && "在局".equals(status)) {
                    String pos = (String)player.get("pos");
                    double stack = ((Number)player.get("stack")).doubleValue();

                    p.append(pos).append(": ").append(String.format("%.0f", stack));

                    // 获取当前街道的最后行动
                    String lastAction = getLastAction(request.getActionsHistory(), pos, request.getStreet());
                    if (lastAction != null) {
                        p.append(" (").append(lastAction).append(")");
                    }
                    p.append("\n");
                    count++;
                }
            }

            if (count == 0) {
                p.append("No active opponents\n");
            }

        } catch (Exception e) {
            p.append("Error parsing opponent data\n");
        }
    }

    /**
     * 获取玩家在当前街道的最后行动
     */
    private String getLastAction(List<PokerAIRequest.ActionHistory> actions, String position, String street) {
        if (actions == null)
            return null;

        for (int i = actions.size() - 1; i >= 0; i--) {
            PokerAIRequest.ActionHistory action = actions.get(i);
            if (position.equals(action.getPosition()) && street.equals(action.getStreet())) {
                return action.getAction();
            }
        }
        return null;
    }

    /**
     * 行动历史（使用英文术语）
     */
    private void appendActionsWithEnglish(StringBuilder p, List<PokerAIRequest.ActionHistory> actions,
        String currentStreet) {
        if (actions == null || actions.isEmpty()) {
            p.append("No actions yet\n");
            return;
        }

        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet() != null ? action.getStreet() : currentStreet;
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }

        // 使用英文术语输出
        String[] streetOrder = {"preflop", "flop", "turn", "river"};
        for (String street : streetOrder) {
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty())
                continue;

            p.append(street).append(": ");

            List<String> compactActions = new ArrayList<>();
            for (PokerAIRequest.ActionHistory action : streetActions) {
                String pos = action.getPosition().replaceAll("[^A-Z]", "");
                String act = action.getAction(); // 保持英文原样
                compactActions.add(pos + ":" + act);
            }

            p.append(String.join(" - ", compactActions));
            p.append("\n");
        }
    }

    /**
     * 描述Hero手牌（例如：顶对、同花听牌等）
     */
    private String describeHeroHand(String heroHole, String board) {
        if (heroHole == null || board == null)
            return null;

        // 简化示例：识别顶对
        String[] holeCards = heroHole.trim().split("\\s+");
        String[] boardCards = board.trim().split("\\s+");

        if (holeCards.length >= 2 && boardCards.length > 0) {
            // 检查是否有对子
            String rank1 = holeCards[0].substring(0, holeCards[0].length() - 1);
            String rank2 = holeCards[1].substring(0, holeCards[1].length() - 1);
            String boardRank = boardCards[0].substring(0, boardCards[0].length() - 1);

            if (rank1.equals(boardRank) || rank2.equals(boardRank)) {
                return "顶对";
            }
        }

        return "高牌";
    }

    /**
     * 分析牌面纹理
     */
    private String analyzeBoardTexture(String board) {
        if (board == null || board.isEmpty())
            return null;

        String[] cards = board.trim().split("\\s+");
        if (cards.length < 3)
            return null;

        // 检查花色
        Set<String> suits = new HashSet<>();
        for (String card : cards) {
            if (card.length() > 0) {
                suits.add(card.substring(card.length() - 1));
            }
        }

        String texture = "";
        if (suits.size() == 3) {
            texture = "彩虹面";
        } else if (suits.size() == 2) {
            texture = "两色面";
        } else {
            texture = "单色面";
        }

        return texture;
    }

    /**
     * 对手分析（只显示还在局的对手）
     */
    private void appendOpponentsAnalysis(StringBuilder p, PokerAIRequest request) {
        if (request.getActionsHistory() == null || request.getActionsHistory().isEmpty()) {
            p.append("暂无对手信息\n\n");
            return;
        }

        // 获取当前街道最后的行动
        String currentStreet = request.getStreet();
        List<PokerAIRequest.ActionHistory> currentStreetActions =
            request.getActionsHistory().stream().filter(a -> currentStreet.equals(a.getStreet()))
                .collect(java.util.stream.Collectors.toList());

        if (currentStreetActions.isEmpty()) {
            p.append("本轮尚无行动\n\n");
            return;
        }

        // 显示对手最后行动
        Map<String, String> lastActions = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : currentStreetActions) {
            if (!action.getPosition().contains("BTN")) { // 排除Hero
                lastActions.put(action.getPosition(), translateActionSimple(action.getAction()));
            }
        }

        for (Map.Entry<String, String> entry : lastActions.entrySet()) {
            p.append(translatePosition(entry.getKey())).append(": ").append(entry.getValue()).append(" ");
        }
        p.append("\n\n");
    }

    /**
     * 压缩的行动历史（每个街道一行）
     */
    private void appendCompactActions(StringBuilder p, List<PokerAIRequest.ActionHistory> actions,
        String currentStreet) {
        if (actions == null || actions.isEmpty()) {
            p.append("暂无行动\n\n");
            return;
        }

        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet() != null ? action.getStreet() : currentStreet;
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }

        // 压缩输出
        String[] streetOrder = {"preflop", "flop", "turn", "river"};
        for (String street : streetOrder) {
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty())
                continue;

            p.append(translateStreet(street)).append(": ");

            // 压缩显示：位置-行动
            List<String> compactActions = new ArrayList<>();
            for (PokerAIRequest.ActionHistory action : streetActions) {
                String posShort = action.getPosition().replaceAll("[^A-Z]", ""); // 只保留大写字母
                String actShort = translateActionSimple(action.getAction()).substring(0, 1); // 只取首字
                compactActions.add(posShort + actShort);
            }

            p.append(String.join("-", compactActions));
            p.append("\n");
        }
        p.append("\n");
    }

    /**
     * 显示详细局势（统一单位）
     */
    private void appendSituationDetail(StringBuilder p, PokerAIRequest request) {
        double bigBlindCents = 200.0; // BB=200美分

        // 底池（只显示数字）
        if (request.getPotBb() != null) {
            double potCents = request.getPotBb() * bigBlindCents;
            p.append("底池: ").append(String.format("%.0f", potCents));
        }

        // 需跟注（只显示数字）
        String toCallInfo = extractToCallAmount(request.getPotOddsNote(), bigBlindCents);
        if (toCallInfo != null) {
            p.append(" | ").append(toCallInfo);
        }

        // SPR（筹码底池比）
        if (request.getSpr() != null) {
            p.append(" | SPR: ").append(String.format("%.1f", request.getSpr()));
            String sprAdvice = request.getSpr() < 5 ? "（浅筹码，全下场景）"
                : request.getSpr() < 13 ? "（中筹码，标准打法）" : "（深筹码，操作空间大）";
            p.append(sprAdvice);
        }
        p.append("\n");
    }

    /**
     * 详细决策上下文
     */
    private void appendDecisionContext(StringBuilder p, PokerAIRequest request) {
        p.append("Hero位于").append(translatePosition(request.getHeroPos()));
        p.append("，当前需要决策。\n");

        // 可选行动
        p.append("可选行动: ");
        String toCallInfo = extractToCallAmount(request.getPotOddsNote());
        if (toCallInfo != null && toCallInfo.contains("无需跟注")) {
            p.append("过牌(check)、下注(bet)\n");
        } else {
            p.append("弃牌(fold)、跟注(call)、加注(raise)\n");
        }

        // 决策考虑要点
        p.append("决策要点: 综合考虑牌力、位置、对手行为、数学期望、筹码深度\n");
    }

    /**
     * 提取需跟注金额（只显示数字）
     */
    private String extractToCallAmount(String potOddsNote, double bigBlindCents) {
        if (potOddsNote == null)
            return null;

        if (potOddsNote.contains("需跟注$")) {
            int start = potOddsNote.indexOf("需跟注$") + 4;
            int end = potOddsNote.indexOf("，", start);
            if (end > start) {
                String amountStr = potOddsNote.substring(start, end);
                try {
                    double cents = Double.parseDouble(amountStr);
                    return String.format("需跟注: %.0f", cents);
                } catch (NumberFormatException e) {
                    return "需跟注: " + amountStr;
                }
            }
        } else if (potOddsNote.contains("无需跟注")) {
            return "无需跟注";
        }

        return null;
    }

    /**
     * 显示详细玩家信息（含PlayerId）
     */
    private void appendPlayersDetailWithId(StringBuilder p, String playersSummary, String heroPos, Double stacksBb) {
        if (playersSummary == null || playersSummary.isEmpty()) {
            // 降级方案：只显示Hero信息
            p.append("Hero: ").append(translatePosition(heroPos));
            if (stacksBb != null) {
                double stackCents = stacksBb * 200.0;
                p.append(" | 筹码: ").append(String.format("%.0f", stackCents));
            }
            p.append("\n");
            return;
        }

        try {
            // 解析玩家摘要JSON
            @SuppressWarnings("unchecked") List<Map<String, Object>> players =
                JsonUtil.fromJson(playersSummary, List.class);
            if (players == null || players.isEmpty()) {
                p.append("Hero: ").append(translatePosition(heroPos)).append("\n");
                return;
            }

            for (Map<String, Object> player : players) {
                int seat = ((Number)player.get("seat")).intValue();
                String pos = (String)player.get("pos");
                double stack = ((Number)player.get("stack")).doubleValue();
                String status = (String)player.get("status");
                boolean isHero = (Boolean)player.get("isHero");

                // 格式化输出（含PlayerId）
                if (isHero) {
                    p.append("Hero ");
                }
                p.append(translatePosition(pos));

                // 添加PlayerId（脱敏显示）
                if (!isHero) {
                    p.append("(P").append(String.format("%03d", seat * 100)).append(")");
                }

                p.append("(座位").append(seat).append("): ");
                p.append(String.format("%.0f", stack));
                p.append(" - ").append(status);
                p.append("\n");
            }

        } catch (Exception e) {
            // 解析失败，使用降级方案
            p.append("Hero: ").append(translatePosition(heroPos));
            if (stacksBb != null) {
                double stackCents = stacksBb * 200.0;
                p.append(" | 筹码: ").append(String.format("%.0f", stackCents));
            }
            p.append("\n");
        }
    }

    /**
     * 显示详细玩家信息（原版：不含PlayerId）
     */
    private void appendPlayersDetail(StringBuilder p, String playersSummary, String heroPos, Double stacksBb) {
        if (playersSummary == null || playersSummary.isEmpty()) {
            // 降级方案：只显示Hero信息
            p.append("Hero: ").append(translatePosition(heroPos));
            if (stacksBb != null) {
                double stackCents = stacksBb * 200.0;
                p.append(" | 筹码: ").append(String.format("%.0f", stackCents));
            }
            return;
        }

        try {
            // 解析玩家摘要JSON
            @SuppressWarnings("unchecked") List<Map<String, Object>> players =
                JsonUtil.fromJson(playersSummary, List.class);
            if (players == null || players.isEmpty()) {
                p.append("Hero: ").append(translatePosition(heroPos));
                return;
            }

            for (Map<String, Object> player : players) {
                int seat = ((Number)player.get("seat")).intValue();
                String pos = (String)player.get("pos");
                double stack = ((Number)player.get("stack")).doubleValue();
                String status = (String)player.get("status");
                boolean isHero = (Boolean)player.get("isHero");

                // 格式化输出
                if (isHero) {
                    p.append("Hero ");
                }
                p.append(translatePosition(pos));
                p.append("(座位").append(seat).append("): ");
                p.append(String.format("%.0f", stack));
                p.append(" - ").append(status);
                p.append("\n");
            }

        } catch (Exception e) {
            // 解析失败，使用降级方案
            p.append("Hero: ").append(translatePosition(heroPos));
            if (stacksBb != null) {
                double stackCents = stacksBb * 200.0;
                p.append(" | 筹码: ").append(String.format("%.0f", stackCents));
            }
        }
    }

    /**
     * 从potOddsNote中提取需要跟注的金额
     */
    private String extractToCallAmount(String potOddsNote) {
        if (potOddsNote == null)
            return null;

        // 解析 "需跟注$600" 格式
        if (potOddsNote.contains("需跟注$")) {
            int start = potOddsNote.indexOf("需跟注$") + 4;
            int end = potOddsNote.indexOf("，", start);
            if (end > start) {
                String amount = potOddsNote.substring(start, end);
                return "需跟注: $" + amount;
            }
        } else if (potOddsNote.contains("无需跟注")) {
            return "无需跟注";
        }

        return null;
    }

    /**
     * 分组显示行动序列（按街道，连续编号）
     */
    private void appendGroupedActions(StringBuilder p, List<PokerAIRequest.ActionHistory> actions,
        String currentStreet) {
        // 按街道分组
        Map<String, List<PokerAIRequest.ActionHistory>> actionsByStreet = new LinkedHashMap<>();
        for (PokerAIRequest.ActionHistory action : actions) {
            String street = action.getStreet() != null ? action.getStreet() : currentStreet;
            actionsByStreet.computeIfAbsent(street, k -> new ArrayList<>()).add(action);
        }

        // 按街道顺序输出（使用连续编号）
        String[] streetOrder = {"preflop", "flop", "turn", "river"};
        int globalIdx = 1;

        for (String street : streetOrder) {
            List<PokerAIRequest.ActionHistory> streetActions = actionsByStreet.get(street);
            if (streetActions == null || streetActions.isEmpty())
                continue;

            // 街道标题（多街道时显示）
            if (actionsByStreet.size() > 1) {
                p.append("\n").append(translateStreet(street)).append(":\n");
            }

            // 输出该街道的行动（连续编号）
            for (PokerAIRequest.ActionHistory action : streetActions) {
                p.append(globalIdx++).append(". ");
                p.append(translatePosition(action.getPosition()));
                p.append(": ").append(translateActionSimple(action.getAction()));
                if (action.getSize() != null) {
                    p.append(" ").append(action.getSize());
                }
                p.append("\n");
            }
        }
    }

    /**
     * 翻译行动类型（简化版本）
     */
    private String translateActionSimple(String action) {
        if (action == null)
            return "未知";
        switch (action.toLowerCase()) {
            case "fold":
                return "弃牌";
            case "call":
                return "跟注";
            case "raise":
                return "加注";
            case "bet":
                return "下注";
            case "check":
                return "过牌";
            case "all-in":
            case "allin":
                return "全下";
            case "small_blind":
                return "下小盲";
            case "big_blind":
                return "下大盲";
            default:
                return action;
        }
    }

    /**
     * 获取GTO标准胜率阈值（V4.15新增，V4.30文档强化）
     * 
     * 【定义】
     * GTO阈值表示该位置"raise-first-in"（开池加注）的最低胜率要求。
     * 这是基于6-max GTO范围统计得出的平均胜率。
     * 
     * 【用途】
     * 1. Preflop开池决策：胜率 > 阈值 → 倾向raise；胜率 < 阈值 → 倾向fold
     * 2. Prompt参考信息：提供给AI作为位置强度的参考
     * 3. 不是绝对规则：实际决策还需考虑GTO混合策略、范围、SPR等因素
     * 
     * 【重要】
     * - 这是"简化的启发式阈值"，不能替代完整的GTO分析
     * - 例如：BB胜率29.3% > 阈值25.0%，但vs对手缩窄范围仍应fold
     * - 真实GTO是混合策略，不只是简单的胜率比较
     * 
     * 【数据来源】
     * 基于PioSolver的6-max Preflop RFI范围统计
     * 
     * @param position 位置（UTG/MP/CO/BTN/SB/BB）
     * @return 该位置的raise-first-in平均胜率阈值（0-1）
     */
    private double getGTOEquityThreshold(String position) {
        if (position == null) {
            return 0.40; // 默认40%（保守）
        }
        
        switch (position.toUpperCase()) {
            case "UTG":
            case "EP":
                // 早位最紧：约15%牌（77+, ATs+, KQs, AQo+），平均胜率35%
                return 0.35;
            case "MP":
                // 中位略宽：约18%牌（66+, A9s+, KTs+, QJs, AJo+），平均胜率33%
                return 0.33;
            case "CO":
                // 后位扩展：约25%牌（55+, A7s+, K9s+, QTs+, JTs, ATo+, KJo+），平均胜率30%
                return 0.30;
            case "BTN":
                // 按钮最宽：约45%牌（22+, A2s+, K5s+, Q8s+, J8s+, T8s+, 98s, A8o+, K9o+, QJo），平均胜率28%
                return 0.28;
            case "SB":
                // 小盲位置劣势：约20%牌（77+, A8s+, KTs+, QJs, ATo+, KQo），平均胜率32%
                return 0.32;
            case "BB":
                // 大盲防守宽泛：约40%牌（22+, A2s+, K2s+, Q6s+, J7s+, T7s+, 97s+, 87s, A2o+, K7o+, Q9o+, J9o+, T9o），平均胜率25%
                // 注意：这是vs单一加注的防守范围，实际决策还要考虑加注尺寸、SPR、对手范围等
                return 0.25;
            default:
                return 0.35; // 默认UTG标准（保守）
        }
    }
    
    /**
     * 解析AI响应为结构化数据
     */
    private PokerAIResponse parseAIResponse(String rawResponse) {
        try {
            // 提取JSON内容
            String jsonContent = rawResponse;
            if (rawResponse.contains("```json")) {
                int start = rawResponse.indexOf("```json") + 7;
                int end = rawResponse.lastIndexOf("```");
                if (end > start) {
                    jsonContent = rawResponse.substring(start, end).trim();
                }
            } else if (rawResponse.contains("{")) {
                int start = rawResponse.indexOf("{");
                int end = rawResponse.lastIndexOf("}") + 1;
                if (end > start) {
                    jsonContent = rawResponse.substring(start, end);
                }
            }

            PokerAIResponse result = JsonUtil.fromJson(jsonContent, PokerAIResponse.class);

            // JsonUtil.fromJson 在解析失败时返回 null，直接抛异常
            if (result == null) {
                throw new RuntimeException(
                    "Failed to parse AI response - JsonUtil returned null, raw response: " + rawResponse);
            }

            return result;
        } catch (Exception e) {
            throw new RuntimeException(
                "Failed to parse AI response: " + e.getMessage() + ", raw response: " + rawResponse, e);
        }
    }

    /**
     * 获取提供商名称
     */
    private String getProviderName(ChatClient chatClient) {
        if (chatClient == null) {
            return "none";
        }
        if (chatClient == qwenChatClient || (chatClient == defaultChatClient && "qwen".equalsIgnoreCase(provider))) {
            return "qwen";
        } else if (chatClient == siliconflowChatClient || (chatClient == defaultChatClient && "siliconflow".equalsIgnoreCase(provider))) {
            return "siliconflow";
        }
        return provider != null ? provider : "unknown";
    }

    /**
     * 确定实际使用的模型名称
     */
    private String determineActualModel(org.springframework.ai.chat.model.ChatResponse chatResponse, String provider) {
        try {
            // 从metadata中获取模型信息
            String metadataModel = chatResponse.getMetadata().getModel();
            if (metadataModel != null && !metadataModel.trim().isEmpty()) {
                return metadataModel;
            }

            // 根据provider推断默认模型
            switch (provider) {
                case "qwen":
                    return "qwen-turbo";
                case "siliconflow":
                case "openai":
                    return "gpt-4o-mini";
                default:
                    return "unknown";
            }
        } catch (Exception e) {
            return "unknown";
        }
    }
    
    /**
     * V4.30: 行动历史分析（多轮加注警告）
     */
    private void appendActionHistoryAnalysis(StringBuilder p, PokerAIRequest request) {
        String currentStreet = request.getStreet();
        long raiseCount = countRaises(request.getActionsHistory(), currentStreet);
        
        // 只在翻前进行多轮加注分析
        if (!"preflop".equalsIgnoreCase(currentStreet)) {
            return;
        }
        
        // 3轮及以上加注需要警告
        if (raiseCount >= 3) {
            p.append("=== 行动历史分析 ===\n");
            p.append("⚠️ 警告：当前已经是第").append(raiseCount).append("轮加注\n");
            
            // 对手范围评估
            String opponentRange = estimateOpponentRangeByRaises(raiseCount);
            p.append("对手范围极度收缩：").append(opponentRange).append("\n");
            
            // 根据轮次给出具体建议
            if (raiseCount == 3) {
                p.append("建议：除非你有{QQ+, AKs}，否则应该认真考虑fold\n");
                p.append("继续raise风险较高，对手通常持有强牌\n");
            } else if (raiseCount == 4) {
                p.append("建议：除非你有{KK+}，否则强烈建议fold\n");
                p.append("继续raise风险极高，对手几乎只有顶级牌\n");
            } else {
                p.append("建议：除非你有{AA, KK}，否则必须fold\n");
                p.append("继续raise几乎肯定会遇到更强的牌，EV为负\n");
                p.append("这是典型的\"坚果牌对决\"场景，中等牌力无法继续\n");
            }
            
            // 重新评估胜率
            Double playerEquity = request.getPlayerEquity() != null ? request.getPlayerEquity() : request.getHeroEquity();
            if (playerEquity != null) {
                p.append("\n重要提示：你的胜率").append(String.format("%.1f", playerEquity * 100));
                p.append("是基于宽范围计算的，但对手范围已极度收缩\n");
                p.append("对抗").append(opponentRange).append("，你的实际胜率会大幅降低\n");
            }
            
            p.append("\n");
        }
    }
    
    /**
     * V4.30: 统计指定街道的加注轮次
     */
    private long countRaises(List<PokerAIRequest.ActionHistory> actions, String targetStreet) {
        if (actions == null || actions.isEmpty()) {
            return 0;
        }
        return actions.stream()
                .filter(a -> targetStreet.equalsIgnoreCase(a.getStreet()))
                .filter(a -> "raise".equalsIgnoreCase(a.getAction()))
                .count();
    }
    
    /**
     * V4.30: 根据加注轮次评估对手范围
     */
    private String estimateOpponentRangeByRaises(long raiseCount) {
        if (raiseCount <= 1) {
            return "宽范围（常规开池或反加注）";
        } else if (raiseCount == 2) {
            return "{TT+, AK, maybe AQs, KQs}";
        } else if (raiseCount == 3) {
            return "{QQ+, AKs, maybe AKo}";
        } else if (raiseCount == 4) {
            return "{KK+, maybe AKs}";
        } else {
            return "{AA, KK}（坚果牌级别）";
        }
    }
}

